Subject: [PATCH] apache2: preserve DetectionOnly semantics for request body errors The fix for CVE-2025-54571 correctly identified that AP_FILTER_ERROR returned from Apache request body input handling must not be ignored by hook_request_late(). Continuing after that condition can leave Apache and ModSecurity in an inconsistent request/response state, so AP_FILTER_ERROR must be treated as a hard Apache-level error regardless of SecRuleEngine mode. However, the 2.9.12 change also changed the return-code contract of read_request_body(). ModSecurity-internal request body processing errors that previously used internal negative return codes, such as -1 and -5, were converted into HTTP status codes. hook_request_late() then started returning any non-OK value directly to Apache. That has an unintended side effect: SecRuleEngine DetectionOnly can now abort uploads for ModSecurity-internal request body inspection conditions, including request body parser/store failures and request body limit conditions. Prior to 2.9.12 those conditions were handled by the existing ModSecurity request body error path and were not automatically converted into hook-level HTTP responses while running in DetectionOnly mode. Keep the security-relevant part of the CVE fix by handling AP_FILTER_ERROR (-3) explicitly before the enforcing-mode check and returning an HTTP error for that condition in all rule engine modes. At the same time, restore the pre-2.9.12 internal return-code contract for ModSecurity request body processing errors so that DetectionOnly does not unexpectedly become a blocking mode for uploads. This keeps AP_FILTER_ERROR fatal, while restoring the previous DetectionOnly behavior for ModSecurity-internal request body errors. --- diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c --- a/apache2/apache2_io.c 2025-08-05 19:08:06.000000000 +0000 +++ b/apache2/apache2_io.c 2026-06-29 06:01:42.904195722 +0000 @@ -192,29 +192,27 @@ if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Input filter: This request does not have a body."); } - return APR_SUCCESS; + return 0; } if (msr->txcfg->reqbody_access != 1) { if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Input filter: Request body access not enabled."); } - return APR_SUCCESS; + return 0; } if (msr->txcfg->debuglog_level >= 4) { msr_log(msr, 4, "Input filter: Reading request body."); } if (modsecurity_request_body_start(msr, error_msg) < 0) { - return HTTP_INTERNAL_SERVER_ERROR; + return -1; } finished_reading = 0; msr->if_seen_eos = 0; bb_in = apr_brigade_create(msr->mp, r->connection->bucket_alloc); - if (bb_in == NULL) { - return HTTP_INTERNAL_SERVER_ERROR; - } + if (bb_in == NULL) return -1; do { apr_status_t rc; @@ -224,17 +222,25 @@ * too large and APR_EGENERAL when the client disconnects. */ switch(rc) { + case APR_INCOMPLETE : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc)); + return -7; + case APR_EOF : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc)); + return -6; + case APR_TIMEUP : + *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc)); + return -4; case AP_FILTER_ERROR : *error_msg = apr_psprintf(msr->mp, "Error reading request body: HTTP Error 413 - Request entity too large. (Most likely.)"); - break; + return -3; case APR_EGENERAL : *error_msg = apr_psprintf(msr->mp, "Error reading request body: Client went away."); - break; + return -2; default : *error_msg = apr_psprintf(msr->mp, "Error reading request body: %s", get_apr_error(msr->mp, rc)); - break; + return -1; } - return ap_map_http_request_error(rc, HTTP_BAD_REQUEST); } /* Loop through the buckets in the brigade in order @@ -250,7 +256,7 @@ rc = apr_bucket_read(bucket, &buf, &buflen, APR_BLOCK_READ); if (rc != APR_SUCCESS) { *error_msg = apr_psprintf(msr->mp, "Failed reading input / bucket (%d): %s", rc, get_apr_error(msr->mp, rc)); - return HTTP_INTERNAL_SERVER_ERROR; + return -1; } if (msr->txcfg->debuglog_level >= 9) { @@ -263,7 +269,7 @@ if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { *error_msg = apr_psprintf(msr->mp, "Request body is larger than the " "configured limit (%ld).", msr->txcfg->reqbody_limit); - return HTTP_REQUEST_ENTITY_TOO_LARGE; + return -5; } else if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) { *error_msg = apr_psprintf(msr->mp, "Request body is larger than the " @@ -284,7 +290,7 @@ *error_msg = apr_psprintf(msr->mp, "Request body is larger than the " "configured limit (%ld).", msr->txcfg->reqbody_limit); - return HTTP_REQUEST_ENTITY_TOO_LARGE; + return -5; } } @@ -294,7 +300,7 @@ modsecurity_request_body_to_stream(msr, buf, buflen, error_msg); #else if (modsecurity_request_body_to_stream(msr, buf, buflen, error_msg) < 0) { - return HTTP_INTERNAL_SERVER_ERROR; + return -1; } #endif } @@ -313,7 +319,7 @@ if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { *error_msg = apr_psprintf(msr->mp, "Request body no files data length is larger than the " "configured limit (%ld).", msr->txcfg->reqbody_no_files_limit); - return HTTP_REQUEST_ENTITY_TOO_LARGE; + return -5; } else if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) { *error_msg = apr_psprintf(msr->mp, "Request body no files data length is larger than the " "configured limit (%ld).", msr->txcfg->reqbody_no_files_limit); @@ -323,12 +329,12 @@ } else { *error_msg = apr_psprintf(msr->mp, "Request body no files data length is larger than the " "configured limit (%ld).", msr->txcfg->reqbody_no_files_limit); - return HTTP_REQUEST_ENTITY_TOO_LARGE; + return -5; } } if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) - return HTTP_INTERNAL_SERVER_ERROR; + return -1; } } @@ -351,16 +357,9 @@ msr->if_status = IF_STATUS_WANTS_TO_RUN; - if (rcbe == -5) { - return HTTP_REQUEST_ENTITY_TOO_LARGE; - } - if (rcbe < 0) { - return HTTP_INTERNAL_SERVER_ERROR; - } - return APR_SUCCESS; + return rcbe; } - /* -- Output filter -- */ /** diff --git a/apache2/mod_security2.c b/apache2/mod_security2.c --- a/apache2/mod_security2.c 2025-08-05 19:08:06.000000000 +0000 +++ b/apache2/mod_security2.c 2026-06-29 06:01:42.905323391 +0000 @@ -1032,15 +1032,65 @@ } rc = read_request_body(msr, &my_error_msg); - if (rc != OK) { + if (rc == -3) { /* AP_FILTER_ERROR. */ if (my_error_msg != NULL) { msr_log(msr, 1, "%s", my_error_msg); } - if (rc == HTTP_REQUEST_ENTITY_TOO_LARGE) { - msr->inbound_error = 1; - } + msr->inbound_error = 1; r->connection->keepalive = AP_CONN_CLOSE; - return rc; + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } + + if (rc < 0 && msr->txcfg->is_enabled == MODSEC_ENABLED) { + switch(rc) { + case -1 : + if (my_error_msg != NULL) { + msr_log(msr, 1, "%s", my_error_msg); + } + return HTTP_INTERNAL_SERVER_ERROR; + break; + case -4 : /* Timeout. */ + if (my_error_msg != NULL) { + msr_log(msr, 4, "%s", my_error_msg); + } + r->connection->keepalive = AP_CONN_CLOSE; + return HTTP_REQUEST_TIME_OUT; + break; + case -5 : /* Request body limit reached. */ + msr->inbound_error = 1; + if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { + r->connection->keepalive = AP_CONN_CLOSE; + if (my_error_msg != NULL) { + msr_log(msr, 1, "%s. Deny with code (%d)", my_error_msg, HTTP_REQUEST_ENTITY_TOO_LARGE); + } + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } else { + if (my_error_msg != NULL) { + msr_log(msr, 1, "%s", my_error_msg); + } + } + break; + case -6 : /* EOF when reading request body. */ + if (my_error_msg != NULL) { + msr_log(msr, 4, "%s", my_error_msg); + } + r->connection->keepalive = AP_CONN_CLOSE; + return HTTP_BAD_REQUEST; + break; + case -7 : /* Partial recieved */ + if (my_error_msg != NULL) { + msr_log(msr, 4, "%s", my_error_msg); + } + r->connection->keepalive = AP_CONN_CLOSE; + return HTTP_BAD_REQUEST; + break; + default : + /* allow through */ + break; + } + + msr->msc_reqbody_error = 1; + msr->msc_reqbody_error_msg = my_error_msg; } /* Update the request headers. They might have changed after