diff --git a/CHANGES b/CHANGES index bd036415..2ae2c712 100644 --- a/CHANGES +++ b/CHANGES @@ -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 ------------------- diff --git a/apache2/apache2_config.c b/apache2/apache2_config.c index 80f8f2b5..4ea098f3 100644 --- a/apache2/apache2_config.c +++ b/apache2/apache2_config.c @@ -50,6 +50,7 @@ void *create_directory_config(apr_pool_t *mp, char *path) dcfg->reqbody_inmemory_limit = NOT_SET; dcfg->reqbody_limit = NOT_SET; dcfg->reqbody_no_files_limit = NOT_SET; + dcfg->reqbody_json_depth_limit = NOT_SET; dcfg->resbody_access = NOT_SET; 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); merged->reqbody_no_files_limit = (child->reqbody_no_files_limit == NOT_SET ? 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 ? 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; 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_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->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; @@ -1920,6 +1924,24 @@ static const char *cmd_request_body_no_files_limit(cmd_parms *cmd, void *_dcfg, 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, 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." ), + 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 ( "SecRequestEncoding", cmd_request_encoding, diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 11313b9b..261151ba 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -95,6 +95,7 @@ typedef struct msc_parm msc_parm; #define REQUEST_BODY_DEFAULT_INMEMORY_LIMIT 131072 #define REQUEST_BODY_DEFAULT_LIMIT 134217728 #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_HARD_LIMIT 1073741824L @@ -498,6 +499,7 @@ struct directory_config { long int reqbody_inmemory_limit; long int reqbody_limit; long int reqbody_no_files_limit; + long int reqbody_json_depth_limit; int resbody_access; long int of_limit; diff --git a/apache2/msc_json.c b/apache2/msc_json.c index c3066032..d69e9eb7 100644 --- a/apache2/msc_json.c +++ b/apache2/msc_json.c @@ -164,6 +164,11 @@ static int yajl_start_array(void *ctx) { else { 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) { 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->current_depth--; return 1; } @@ -229,6 +235,11 @@ static int yajl_start_map(void *ctx) else { 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) { 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->prefix = (unsigned char *) NULL; } + msr->json->current_depth--; return 1; } @@ -308,6 +320,9 @@ int json_init(modsec_rec *msr, char **error_msg) { msr->json->prefix = (unsigned char *) NULL; msr->json->current_key = (unsigned char *) NULL; + msr->json->current_depth = 0; + msr->json->depth_limit_exceeded = 0; + /** * 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); if (msr->json->status != yajl_status_ok) { /* 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; } @@ -357,7 +376,12 @@ int json_complete(modsec_rec *msr, char **error_msg) { msr->json->status = yajl_complete_parse(msr->json->handle); if (msr->json->status != yajl_status_ok) { /* 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; } diff --git a/apache2/msc_json.h b/apache2/msc_json.h index 02326ec0..7e3d7250 100644 --- a/apache2/msc_json.h +++ b/apache2/msc_json.h @@ -40,6 +40,8 @@ struct json_data { /* prefix is used to create data hierarchy (i.e., 'parent.child.value') */ unsigned char *prefix; unsigned char *current_key; + long int current_depth; + int depth_limit_exceeded; }; /* Functions */ diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 181df9e0..f84355a9 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -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"}}}} + } + ), + ), + ), } +