Merge pull request #2644 from martinhsv/v2/master

Support configurable limit on depth of JSON parsing
This commit is contained in:
martinhsv
2021-11-18 20:49:28 -05:00
committed by GitHub
6 changed files with 135 additions and 2 deletions

View File

@@ -1,3 +1,9 @@
DD mmm YYYY - 2.9.x (to be released)
-------------------
* Support configurable limit on depth of JSON parsing
[@theMiddleBlue, @airween, @dune73, @martinhsv]
21 Jun 2021 - 2.9.4 21 Jun 2021 - 2.9.4
------------------- -------------------

View File

@@ -50,6 +50,7 @@ void *create_directory_config(apr_pool_t *mp, char *path)
dcfg->reqbody_inmemory_limit = NOT_SET; dcfg->reqbody_inmemory_limit = NOT_SET;
dcfg->reqbody_limit = NOT_SET; dcfg->reqbody_limit = NOT_SET;
dcfg->reqbody_no_files_limit = NOT_SET; dcfg->reqbody_no_files_limit = NOT_SET;
dcfg->reqbody_json_depth_limit = NOT_SET;
dcfg->resbody_access = NOT_SET; dcfg->resbody_access = NOT_SET;
dcfg->debuglog_name = NOT_SET_P; dcfg->debuglog_name = NOT_SET_P;
@@ -332,6 +333,8 @@ void *merge_directory_configs(apr_pool_t *mp, void *_parent, void *_child)
? parent->reqbody_limit : child->reqbody_limit); ? parent->reqbody_limit : child->reqbody_limit);
merged->reqbody_no_files_limit = (child->reqbody_no_files_limit == NOT_SET merged->reqbody_no_files_limit = (child->reqbody_no_files_limit == NOT_SET
? parent->reqbody_no_files_limit : child->reqbody_no_files_limit); ? parent->reqbody_no_files_limit : child->reqbody_no_files_limit);
merged->reqbody_json_depth_limit = (child->reqbody_json_depth_limit == NOT_SET
? parent->reqbody_json_depth_limit : child->reqbody_json_depth_limit);
merged->resbody_access = (child->resbody_access == NOT_SET merged->resbody_access = (child->resbody_access == NOT_SET
? parent->resbody_access : child->resbody_access); ? parent->resbody_access : child->resbody_access);
@@ -648,6 +651,7 @@ void init_directory_config(directory_config *dcfg)
dcfg->reqbody_inmemory_limit = REQUEST_BODY_DEFAULT_INMEMORY_LIMIT; dcfg->reqbody_inmemory_limit = REQUEST_BODY_DEFAULT_INMEMORY_LIMIT;
if (dcfg->reqbody_limit == NOT_SET) dcfg->reqbody_limit = REQUEST_BODY_DEFAULT_LIMIT; if (dcfg->reqbody_limit == NOT_SET) dcfg->reqbody_limit = REQUEST_BODY_DEFAULT_LIMIT;
if (dcfg->reqbody_no_files_limit == NOT_SET) dcfg->reqbody_no_files_limit = REQUEST_BODY_NO_FILES_DEFAULT_LIMIT; if (dcfg->reqbody_no_files_limit == NOT_SET) dcfg->reqbody_no_files_limit = REQUEST_BODY_NO_FILES_DEFAULT_LIMIT;
if (dcfg->reqbody_json_depth_limit == NOT_SET) dcfg->reqbody_json_depth_limit = REQUEST_BODY_JSON_DEPTH_DEFAULT_LIMIT;
if (dcfg->resbody_access == NOT_SET) dcfg->resbody_access = 0; if (dcfg->resbody_access == NOT_SET) dcfg->resbody_access = 0;
if (dcfg->of_limit == NOT_SET) dcfg->of_limit = RESPONSE_BODY_DEFAULT_LIMIT; if (dcfg->of_limit == NOT_SET) dcfg->of_limit = RESPONSE_BODY_DEFAULT_LIMIT;
if (dcfg->if_limit_action == NOT_SET) dcfg->if_limit_action = REQUEST_BODY_LIMIT_ACTION_REJECT; if (dcfg->if_limit_action == NOT_SET) dcfg->if_limit_action = REQUEST_BODY_LIMIT_ACTION_REJECT;
@@ -1920,6 +1924,24 @@ static const char *cmd_request_body_no_files_limit(cmd_parms *cmd, void *_dcfg,
return NULL; return NULL;
} }
static const char *cmd_request_body_json_depth_limit(cmd_parms *cmd, void *_dcfg,
const char *p1)
{
directory_config *dcfg = (directory_config *)_dcfg;
long int limit;
if (dcfg == NULL) return NULL;
limit = strtol(p1, NULL, 10);
if ((limit == LONG_MAX)||(limit == LONG_MIN)||(limit <= 0)) {
return apr_psprintf(cmd->pool, "ModSecurity: Invalid value for SecRequestBodyJsonDepthLimit: %s", p1);
}
dcfg->reqbody_json_depth_limit = limit;
return NULL;
}
static const char *cmd_request_body_access(cmd_parms *cmd, void *_dcfg, static const char *cmd_request_body_access(cmd_parms *cmd, void *_dcfg,
const char *p1) const char *p1)
{ {
@@ -3553,6 +3575,14 @@ const command_rec module_directives[] = {
"maximum request body size ModSecurity will accept, but excluding the size of uploaded files." "maximum request body size ModSecurity will accept, but excluding the size of uploaded files."
), ),
AP_INIT_TAKE1 (
"SecRequestBodyJsonDepthLimit",
cmd_request_body_json_depth_limit,
NULL,
CMD_SCOPE_ANY,
"maximum request body JSON parsing depth ModSecurity will accept."
),
AP_INIT_TAKE1 ( AP_INIT_TAKE1 (
"SecRequestEncoding", "SecRequestEncoding",
cmd_request_encoding, cmd_request_encoding,

View File

@@ -95,6 +95,7 @@ typedef struct msc_parm msc_parm;
#define REQUEST_BODY_DEFAULT_INMEMORY_LIMIT 131072 #define REQUEST_BODY_DEFAULT_INMEMORY_LIMIT 131072
#define REQUEST_BODY_DEFAULT_LIMIT 134217728 #define REQUEST_BODY_DEFAULT_LIMIT 134217728
#define REQUEST_BODY_NO_FILES_DEFAULT_LIMIT 1048576 #define REQUEST_BODY_NO_FILES_DEFAULT_LIMIT 1048576
#define REQUEST_BODY_JSON_DEPTH_DEFAULT_LIMIT 10000
#define RESPONSE_BODY_DEFAULT_LIMIT 524288 #define RESPONSE_BODY_DEFAULT_LIMIT 524288
#define RESPONSE_BODY_HARD_LIMIT 1073741824L #define RESPONSE_BODY_HARD_LIMIT 1073741824L
@@ -498,6 +499,7 @@ struct directory_config {
long int reqbody_inmemory_limit; long int reqbody_inmemory_limit;
long int reqbody_limit; long int reqbody_limit;
long int reqbody_no_files_limit; long int reqbody_no_files_limit;
long int reqbody_json_depth_limit;
int resbody_access; int resbody_access;
long int of_limit; long int of_limit;

View File

@@ -164,6 +164,11 @@ static int yajl_start_array(void *ctx) {
else { else {
msr->json->prefix = apr_pstrdup(msr->mp, msr->json->current_key); msr->json->prefix = apr_pstrdup(msr->mp, msr->json->current_key);
} }
msr->json->current_depth++;
if (msr->json->current_depth > msr->txcfg->reqbody_json_depth_limit) {
msr->json->depth_limit_exceeded = 1;
return 0;
}
if (msr->txcfg->debuglog_level >= 9) { if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "New JSON hash context (prefix '%s')", msr->json->prefix); msr_log(msr, 9, "New JSON hash context (prefix '%s')", msr->json->prefix);
@@ -200,6 +205,7 @@ static int yajl_end_array(void *ctx) {
*/ */
msr->json->prefix = (unsigned char *) NULL; msr->json->prefix = (unsigned char *) NULL;
} }
msr->json->current_depth--;
return 1; return 1;
} }
@@ -229,6 +235,11 @@ static int yajl_start_map(void *ctx)
else { else {
msr->json->prefix = apr_pstrdup(msr->mp, msr->json->current_key); msr->json->prefix = apr_pstrdup(msr->mp, msr->json->current_key);
} }
msr->json->current_depth++;
if (msr->json->current_depth > msr->txcfg->reqbody_json_depth_limit) {
msr->json->depth_limit_exceeded = 1;
return 0;
}
if (msr->txcfg->debuglog_level >= 9) { if (msr->txcfg->debuglog_level >= 9) {
msr_log(msr, 9, "New JSON hash context (prefix '%s')", msr->json->prefix); msr_log(msr, 9, "New JSON hash context (prefix '%s')", msr->json->prefix);
@@ -270,6 +281,7 @@ static int yajl_end_map(void *ctx)
msr->json->current_key = msr->json->prefix; msr->json->current_key = msr->json->prefix;
msr->json->prefix = (unsigned char *) NULL; msr->json->prefix = (unsigned char *) NULL;
} }
msr->json->current_depth--;
return 1; return 1;
} }
@@ -308,6 +320,9 @@ int json_init(modsec_rec *msr, char **error_msg) {
msr->json->prefix = (unsigned char *) NULL; msr->json->prefix = (unsigned char *) NULL;
msr->json->current_key = (unsigned char *) NULL; msr->json->current_key = (unsigned char *) NULL;
msr->json->current_depth = 0;
msr->json->depth_limit_exceeded = 0;
/** /**
* yajl initialization * yajl initialization
* *
@@ -337,7 +352,11 @@ int json_process_chunk(modsec_rec *msr, const char *buf, unsigned int size, char
msr->json->status = yajl_parse(msr->json->handle, buf, size); msr->json->status = yajl_parse(msr->json->handle, buf, size);
if (msr->json->status != yajl_status_ok) { if (msr->json->status != yajl_status_ok) {
/* We need to free the yajl error message later, how to do this? */ /* We need to free the yajl error message later, how to do this? */
*error_msg = yajl_get_error(msr->json->handle, 0, buf, size); if (msr->json->depth_limit_exceeded) {
*error_msg = "JSON depth limit exceeded";
} else {
*error_msg = yajl_get_error(msr->json->handle, 0, NULL, 0);
}
return -1; return -1;
} }
@@ -357,7 +376,12 @@ int json_complete(modsec_rec *msr, char **error_msg) {
msr->json->status = yajl_complete_parse(msr->json->handle); msr->json->status = yajl_complete_parse(msr->json->handle);
if (msr->json->status != yajl_status_ok) { if (msr->json->status != yajl_status_ok) {
/* We need to free the yajl error message later, how to do this? */ /* We need to free the yajl error message later, how to do this? */
*error_msg = yajl_get_error(msr->json->handle, 0, NULL, 0); if (msr->json->depth_limit_exceeded) {
*error_msg = "JSON depth limit exceeded";
} else {
*error_msg = yajl_get_error(msr->json->handle, 0, NULL, 0);
}
return -1; return -1;
} }

View File

@@ -40,6 +40,8 @@ struct json_data {
/* prefix is used to create data hierarchy (i.e., 'parent.child.value') */ /* prefix is used to create data hierarchy (i.e., 'parent.child.value') */
unsigned char *prefix; unsigned char *prefix;
unsigned char *current_key; unsigned char *current_key;
long int current_depth;
int depth_limit_exceeded;
}; };
/* Functions */ /* Functions */

View File

@@ -156,5 +156,74 @@
), ),
), ),
), ),
},
{
type => "rule",
comment => "json parser - parsing depth not exceeded",
conf => qq(
SecRuleEngine On
SecRequestBodyAccess On
SecDebugLog $ENV{DEBUG_LOG}
SecDebugLogLevel 9
SecRequestBodyJsonDepthLimit 5
SecRule REQUEST_HEADERS:Content-Type "application/json" \\
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
SecRule REQBODY_ERROR "!\@eq 0" "id:'200442',phase:2,log,deny,status:403,msg:'Failed to parse request body'"
),
match_log => {
debug => [ qr/key/s, 1 ],
},
match_response => {
status => qr/^200$/,
},
request => new HTTP::Request(
POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt",
[
"Content-Type" => "application/json",
],
normalize_raw_request_data(
q(
{
"key1":{"key2":{"key3":{"key4":{"key5":"thevalue"}}}}
}
),
),
),
},
{
type => "rule",
comment => "json parser - parsing depth exceeded",
conf => qq(
SecRuleEngine On
SecRequestBodyAccess On
SecDebugLog $ENV{DEBUG_LOG}
SecAuditEngine RelevantOnly
SecAuditLog "$ENV{AUDIT_LOG}"
SecDebugLogLevel 9
SecRequestBodyJsonDepthLimit 3
SecRule REQUEST_HEADERS:Content-Type "application/json" \\
"id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON"
SecRule REQBODY_ERROR "!\@eq 0" "id:'200443',phase:2,log,deny,status:403,msg:'Failed to parse request body'"
),
match_log => {
audit => [ qr/JSON parsing error: JSON depth limit exceeded/s, 1 ],
},
match_response => {
status => qr/^403$/,
},
request => new HTTP::Request(
POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt",
[
"Content-Type" => "application/json",
],
normalize_raw_request_data(
q(
{
"key1":{"key2":{"key3":{"key4":{"key5":"thevalue"}}}}
}
),
),
),
} }