Description: This patch fixes CVE-2021-42717
 ModSecurity has a DoS Vulnerability in JSON Parsing. The bug
 has descibed in CVE-2021-42717. This patch fixes it.
Author: Ervin Hegedus <airween@gmail.com>

---
Origin: other
Bug: https://github.com/SpiderLabs/ModSecurity/issues/2647
Last-Update: 2021-12-01

--- modsecurity-apache-2.9.3.orig/apache2/apache2_config.c
+++ modsecurity-apache-2.9.3/apache2/apache2_config.c
@@ -50,6 +50,7 @@ void *create_directory_config(apr_pool_t
     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
         ? 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_con
         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_f
     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)
 {
@@ -3554,6 +3576,14 @@ const command_rec module_directives[] =
     ),
 
     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,
         NULL,
--- modsecurity-apache-2.9.3.orig/apache2/modsecurity.h
+++ modsecurity-apache-2.9.3/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
 
@@ -492,6 +493,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;
--- modsecurity-apache-2.9.3.orig/apache2/msc_json.c
+++ modsecurity-apache-2.9.3/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 **er
     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,
     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,11 @@ int json_complete(modsec_rec *msr, char
     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;
     }
 
--- modsecurity-apache-2.9.3.orig/apache2/msc_json.h
+++ modsecurity-apache-2.9.3/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 */
--- modsecurity-apache-2.9.3.orig/tests/regression/rule/15-json.t
+++ modsecurity-apache-2.9.3/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"}}}}
+				}
+			),
+		),
+	),
 }
 
+
