From a682f13270e0f8099db77e2abc7cf893e03624b9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 25 Dec 2025 22:14:43 +0900 Subject: [PATCH 01/25] Allow partial processing of multipart, JSON, and XML request body --- apache2/apache2_io.c | 23 ++-- apache2/modsecurity.h | 2 + apache2/msc_json.c | 4 + apache2/msc_json.h | 2 + apache2/msc_multipart.c | 2 +- apache2/msc_multipart.h | 1 + apache2/msc_reqbody.c | 27 ++++- apache2/msc_xml.c | 4 +- apache2/msc_xml.h | 1 + tests/regression/rule/10-xml.t | 93 +++++++++++++++ tests/regression/rule/15-json.t | 195 +++++++++++++++++++++++++++++++- 11 files changed, 334 insertions(+), 20 deletions(-) diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c index 8deeb01c9a..073fefe128 100644 --- a/apache2/apache2_io.c +++ b/apache2/apache2_io.c @@ -299,15 +299,16 @@ apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { #endif } + if (msr->reqbody_length + buflen > (apr_size_t)msr->txcfg->reqbody_limit && msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { + buflen = (apr_size_t)msr->txcfg->reqbody_limit - msr->reqbody_length; + finished_reading = 1; + modsecurity_request_body_enable_partial_processing(msr); + } + msr->reqbody_length += buflen; if (buflen != 0) { int rcbs = modsecurity_request_body_store(msr, buf, buflen, error_msg); - - if (msr->reqbody_length > (apr_size_t)msr->txcfg->reqbody_limit && msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { - finished_reading = 1; - } - if (rcbs < 0) { if (rcbs == -5) { if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { @@ -351,11 +352,13 @@ apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { 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; + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) { + if (rcbe == -5) { + return HTTP_REQUEST_ENTITY_TOO_LARGE; + } + if (rcbe < 0) { + return HTTP_INTERNAL_SERVER_ERROR; + } } return APR_SUCCESS; } diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 2894717031..ddda27e2b9 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -736,6 +736,8 @@ apr_status_t DSOLOCAL modsecurity_request_body_start(modsec_rec *msr, char **err apr_status_t DSOLOCAL modsecurity_request_body_store(modsec_rec *msr, const char *data, apr_size_t length, char **error_msg); +void DSOLOCAL modsecurity_request_body_enable_partial_processing(modsec_rec *msr); + apr_status_t DSOLOCAL modsecurity_request_body_end(modsec_rec *msr, char **error_msg); apr_status_t DSOLOCAL modsecurity_request_body_to_stream(modsec_rec *msr, const char *buffer, int buflen, char **error_msg); diff --git a/apache2/msc_json.c b/apache2/msc_json.c index cae7bf4f70..55ed6954f8 100644 --- a/apache2/msc_json.c +++ b/apache2/msc_json.c @@ -367,6 +367,10 @@ int json_init(modsec_rec *msr, char **error_msg) { return 1; } +void json_allow_partial_values(modsec_rec *msr) { + (void)yajl_config(msr->json->handle, yajl_allow_partial_values, 1); +} + /** * Feed one chunk of data to the JSON parser. */ diff --git a/apache2/msc_json.h b/apache2/msc_json.h index 089dab4763..c5fbc80df8 100644 --- a/apache2/msc_json.h +++ b/apache2/msc_json.h @@ -48,6 +48,8 @@ struct json_data { int DSOLOCAL json_init(modsec_rec *msr, char **error_msg); +void DSOLOCAL json_allow_partial_values(modsec_rec *msr); + int DSOLOCAL json_process(modsec_rec *msr, const char *buf, unsigned int size, char **error_msg); diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index dc24248de7..c8806c26d9 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1054,7 +1054,7 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { } } - if (msr->mpd->is_complete == 0) { + if (msr->mpd->is_complete == 0 && msr->mpd->allow_process_partial == 0) { *error_msg = apr_psprintf(msr->mp, "Multipart: Final boundary missing."); return -1; } diff --git a/apache2/msc_multipart.h b/apache2/msc_multipart.h index a9c20b9b19..8dfd473812 100644 --- a/apache2/msc_multipart.h +++ b/apache2/msc_multipart.h @@ -124,6 +124,7 @@ struct multipart_data { int seen_data; int is_complete; + int allow_process_partial; int flag_error; int flag_data_before; diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index e00a4fc3fb..508d8ada8f 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -413,12 +413,13 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, msr_log(msr, 1, "%s", *error_msg); } - msr->msc_reqbody_error = 1; + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) + msr->msc_reqbody_error = 1; - if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { + if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { return -5; - } else if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { - if(msr->txcfg->is_enabled == MODSEC_ENABLED) + } else if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { + if (msr->txcfg->is_enabled == MODSEC_ENABLED) return -5; } } @@ -438,6 +439,24 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, return -1; } +/** + * Enable partial processing of request body data. + */ +void modsecurity_request_body_enable_partial_processing(modsec_rec *msr) { + if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { + msr->mpd->allow_process_partial = 1; + msr_log(msr, 4, "Multipart: Allow partial processing of request body"); + } + else if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { + msr->xml->allow_ill_formed = 1; + msr_log(msr, 4, "XML: Allow partial processing of request body"); + } + else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { + json_allow_partial_values(msr); + msr_log(msr, 4, "JSON: Allow partial processing of request body"); + } +} + apr_status_t modsecurity_request_body_to_stream(modsec_rec *msr, const char *buffer, int buflen, char **error_msg) { assert(msr != NULL); assert(error_msg != NULL); diff --git a/apache2/msc_xml.c b/apache2/msc_xml.c index 4f0e07ca05..9222b0176d 100644 --- a/apache2/msc_xml.c +++ b/apache2/msc_xml.c @@ -267,7 +267,7 @@ int xml_process_chunk(modsec_rec *msr, const char *buf, unsigned int size, char if (msr->xml->parsing_ctx != NULL && msr->txcfg->parse_xml_into_args != MSC_XML_ARGS_ONLYARGS) { xmlParseChunk(msr->xml->parsing_ctx, buf, size, 0); - if (msr->xml->parsing_ctx->wellFormed != 1) { + if (!msr->xml->allow_ill_formed && msr->xml->parsing_ctx->wellFormed != 1) { *error_msg = apr_psprintf(msr->mp, "XML: Failed to parse document."); return -1; } @@ -318,7 +318,7 @@ int xml_complete(modsec_rec *msr, char **error_msg) { msr->xml->parsing_ctx = NULL; msr_log(msr, 4, "XML: Parsing complete (well_formed %u).", msr->xml->well_formed); - if (msr->xml->well_formed != 1) { + if (!msr->xml->allow_ill_formed && msr->xml->well_formed != 1) { *error_msg = apr_psprintf(msr->mp, "XML: Failed to parse document."); return -1; } diff --git a/apache2/msc_xml.h b/apache2/msc_xml.h index 73999443a2..b56905dbcd 100644 --- a/apache2/msc_xml.h +++ b/apache2/msc_xml.h @@ -43,6 +43,7 @@ struct xml_data { xmlDocPtr doc; unsigned int well_formed; + unsigned int allow_ill_formed; /* error reporting and XML array flag */ char *xml_error; diff --git a/tests/regression/rule/10-xml.t b/tests/regression/rule/10-xml.t index ad1ed91941..0a5e35eb27 100644 --- a/tests/regression/rule/10-xml.t +++ b/tests/regression/rule/10-xml.t @@ -428,3 +428,96 @@ ), ), }, +{ + type => "rule", + comment => "xml ProcessPartial, bad value and whole body before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 61 + SecXmlExternalEntity Off + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500005, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500006 + SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), +}, +{ + type => "rule", + comment => "xml ProcessPartial, bad value before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 61 + SecXmlExternalEntity Off + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500005, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500006 + SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q(bad_valueok_value), + ), + ), +}, +{ + type => "rule", + comment => "xml ProcessPartial, bad value after limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 61 + SecXmlExternalEntity Off + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500005, \\ + phase:1,t:none,t:lowercase,nolog,pass,ctl:requestBodyProcessor=XML" + SecRule REQBODY_PROCESSOR "!^XML\$" nolog,pass,skipAfter:12345,id:500006 + SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" + ), + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + ], + normalize_raw_request_data( + q(12bad_value), + ), + ), +}, diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 9c17817503..870415647d 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -258,6 +258,195 @@ ), ), ), -} - - +}, +{ + type => "rule", + comment => "LimitAction ProcessPartial, bad value and whole body before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], + debug => [ qr/Adding JSON argument 'b' with value 'bad_value'|JSON support was not enabled/, 1 ], + -debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":1234,"b":"bad_value"}), + ), +}, +{ + type => "rule", + comment => "LimitAction ProcessPartial, bad value before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], + debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":12345,"b":"bad_value"}), + ), +}, +{ + type => "rule", + comment => "LimitAction ProcessPartial, bad value after limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":123456,"b":"bad_value"}), + ), +}, +{ + type => "rule", + comment => "LimitAction Reject, bad value and whole body before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], + debug => [ qr/Adding JSON argument 'b' with value 'bad_value'|JSON support was not enabled/, 1 ], + -debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":1234,"b":"bad_value"}), + ), +}, +{ + type => "rule", + comment => "LimitAction Reject, bad value before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(26\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":12345,"b":"bad_value"}), + ), +}, +{ + type => "rule", + comment => "LimitAction Reject, bad value after limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 26 + SecRequestBodyLimit 26 + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + 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:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(26\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + q({"a":123456,"b":"bad_value"}), + ), +}, From 55270e514d83869f8d187b600772fb2618fff86b Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 31 Dec 2025 05:40:05 +0900 Subject: [PATCH 02/25] Adjust test since ProcessPartial no more causes "no final boundary missing" Due to the previous change, the "no final boundary missing" error never occurs when SecRequestBodyLimitAction is ProcessPartial. --- tests/regression/config/10-request-directives.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 929537e2cd..3690280368 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -578,10 +578,10 @@ SecRequestBodyLimit 131072 ), match_log => { - error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], }, match_response => { - status => qr/^500$/, + status => qr/^200$/, }, request => normalize_raw_request_data( qq( From 586e1ef0c046702142af2c46ebc739952955b9f5 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 30 Dec 2025 09:36:17 +0900 Subject: [PATCH 03/25] Fix json test match_log --- tests/regression/rule/15-json.t | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 870415647d..1e1c93804f 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -279,7 +279,7 @@ match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], debug => [ qr/Adding JSON argument 'b' with value 'bad_value'|JSON support was not enabled/, 1 ], - -debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + -debug => [ qr/JSON: Allow partial processing of request body|JSON support was not enabled/, 1 ], }, match_response => { status => qr/^403$/, @@ -311,7 +311,7 @@ ), match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], - debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + debug => [ qr/JSON: Allow partial processing of request body|JSON support was not enabled/, 1 ], }, match_response => { status => qr/^403$/, @@ -342,7 +342,7 @@ SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { - debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + debug => [ qr/JSON: Allow partial processing of request body|JSON support was not enabled/, 1 ], }, match_response => { status => qr/^200$/, @@ -375,7 +375,7 @@ match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], debug => [ qr/Adding JSON argument 'b' with value 'bad_value'|JSON support was not enabled/, 1 ], - -debug => [ qr/JSON set reqbody_limit_exceeded|JSON support was not enabled/, 1 ], + -debug => [ qr/JSON: Allow partial processing of request body|JSON support was not enabled/, 1 ], }, match_response => { status => qr/^403$/, From e9dca3c2ac48880c6993879d9c81a446ec8ef801 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 24 Dec 2025 23:37:29 +0900 Subject: [PATCH 04/25] Fix expected error message in regression test Adjust the expected error message to match the message changes introdueced in: https://github.com/owasp-modsecurity/ModSecurity/commit/dfbde557acc41d858dbe04d4b6eaec64478347ff --- tests/regression/config/10-request-directives.t | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 3690280368..09eb2a6228 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -501,7 +501,7 @@ SecRequestBodyLimit 20 ), match_log => { - debug => [ qr/Request body is larger than the configured limit \(20\).. Deny with code \(413\)/, 1 ], + debug => [ qr/Request body is larger than the configured limit \(20\)./, 1 ], }, match_response => { status => qr/^413$/, @@ -545,7 +545,7 @@ SecRequestBodyLimit 131072 ), match_log => { - -debug => [ qr/Request body is larger than the configured limit \(131072\).. Deny with code \(413\)/, 1 ], + -debug => [ qr/Request body is larger than the configured limit \(131072\)./, 1 ], }, match_response => { status => qr/^413$/, From 0e4728f46c01619240c4b300d1026113a6ecb595 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 31 Dec 2025 21:40:39 +0900 Subject: [PATCH 05/25] Accept partial epilogue in body larger than limit for ProcessPartial But reject incomplete epilogue when body fits in limit. --- apache2/msc_multipart.c | 33 ++- .../regression/config/10-request-directives.t | 276 ++++++++++++++++++ 2 files changed, 308 insertions(+), 1 deletion(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index c8806c26d9..30008acc72 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1023,13 +1023,44 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { * processed yet) in the buffer. */ if (msr->mpd->buf_contains_line) { - if ( ((unsigned int)(MULTIPART_BUF_SIZE - msr->mpd->bufleft) == (4 + strlen(msr->mpd->boundary))) + /* + * Note that the buffer may end with the final boundary followed by only CR, + * coming from the [CRLF epilogue], when allow_process_partial == 1 (which is + * set when SecRequestBodyLimitAction is ProcessPartial and the request body + * length exceeds SecRequestBodyLimit). + * + * The following definitions are copied from RFC 2046: + * + * dash-boundary := "--" boundary + * + * delimiter := CRLF dash-boundary + * + * close-delimiter := delimiter "--" + * + * multipart-body := [preamble CRLF] + * dash-boundary transport-padding CRLF + * body-part *encapsulation + * close-delimiter transport-padding + * [CRLF epilogue] + */ + unsigned int buf_data_len = (unsigned int)(MULTIPART_BUF_SIZE - msr->mpd->bufleft); + size_t final_boundary_len = 4 + strlen(msr->mpd->boundary); + if ( (buf_data_len >= final_boundary_len) && (*(msr->mpd->buf) == '-') && (*(msr->mpd->buf + 1) == '-') && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) == '-') && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary) + 1) == '-') ) { + /* If body fits in limit and ends with final boundary plus just CR, reject it. */ + if ( (msr->mpd->allow_process_partial == 0) + && (buf_data_len == final_boundary_len + 1) + && (*(msr->mpd->buf + final_boundary_len) == '\r') ) + { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid epilogue after final boundary."); + return -1; + } + if ((msr->mpd->crlf_state_buf_end == 2) && (msr->mpd->flag_lf_line != 1)) { msr->mpd->flag_lf_line = 1; if (msr->mpd->flag_crlf_line) { diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 09eb2a6228..57be89733a 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -643,7 +643,283 @@ # ), #}, +# SecRequestBodyLimitAction ProcessPartial +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/just limit - bad_name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 296 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_NAME "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name2" + value2 + -----------------------------69343412719991675451336310646--), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/greater - bad_name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 295 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_NAME "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name2" + + value2 + -----------------------------69343412719991675451336310646--), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/no epilogue)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 176 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646--), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CR after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 176 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646--) . "\r", + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CR just in limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 177 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/"Multipart: Invalid epilogue after final boundary."/, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646--) . "\r", + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF across limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 177 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CR before limit, non-LF after)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 177 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646--) . "\rbad epilogue after just CR", + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/empty epilogue just in limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 178 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" + + value1 + -----------------------------69343412719991675451336310646-- + ), + ), + ), +}, From 24508d66fda0f17e136aaf5f6b3832c9d27dcecd Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 31 Dec 2025 10:52:40 +0900 Subject: [PATCH 06/25] Fix indent in apache2/msc_multipart.c --- apache2/msc_multipart.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 30008acc72..2ffaf052a5 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1327,10 +1327,10 @@ int multipart_process_chunk(modsec_rec *msr, const char *buf, if (c == 0x0a) { if (msr->mpd->crlf_state == 1) { msr->mpd->crlf_state = 3; - } else { + } else { msr->mpd->crlf_state = 2; - } - } + } + } msr->mpd->crlf_state_buf_end = msr->mpd->crlf_state; } From bea943b985da88ca1ec5b547505f5b424237222c Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 31 Dec 2025 10:55:28 +0900 Subject: [PATCH 07/25] Fix indent in apache2/msc_json.c --- apache2/msc_json.c | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/apache2/msc_json.c b/apache2/msc_json.c index 55ed6954f8..234fea2ebf 100644 --- a/apache2/msc_json.c +++ b/apache2/msc_json.c @@ -187,7 +187,7 @@ static int yajl_start_array(void *ctx) { msr->json->current_depth++; if (msr->json->current_depth > msr->txcfg->reqbody_json_depth_limit) { msr->json->depth_limit_exceeded = 1; - return 0; + return 0; } if (msr->txcfg->debuglog_level >= 9) { @@ -262,7 +262,7 @@ static int yajl_start_map(void *ctx) msr->json->current_depth++; if (msr->json->current_depth > msr->txcfg->reqbody_json_depth_limit) { msr->json->depth_limit_exceeded = 1; - return 0; + return 0; } if (msr->txcfg->debuglog_level >= 9) { @@ -384,16 +384,16 @@ int json_process_chunk(modsec_rec *msr, const char *buf, unsigned int size, char /* Feed our parser and catch any errors */ msr->json->status = yajl_parse(msr->json->handle, buf, size); if (msr->json->status != yajl_status_ok) { - if (msr->json->depth_limit_exceeded) { - *error_msg = "JSON depth limit exceeded"; - } else { - if (msr->json->yajl_error) *error_msg = msr->json->yajl_error; - else { - char* yajl_err = yajl_get_error(msr->json->handle, 0, buf, size); - *error_msg = apr_pstrdup(msr->mp, yajl_err); - yajl_free_error(msr->json->handle, yajl_err); + if (msr->json->depth_limit_exceeded) { + *error_msg = "JSON depth limit exceeded"; + } else { + if (msr->json->yajl_error) *error_msg = msr->json->yajl_error; + else { + char* yajl_err = yajl_get_error(msr->json->handle, 0, buf, size); + *error_msg = apr_pstrdup(msr->mp, yajl_err); + yajl_free_error(msr->json->handle, yajl_err); + } } - } return -1; } @@ -413,13 +413,13 @@ int json_complete(modsec_rec *msr, char **error_msg) { /* Wrap up the parsing process */ msr->json->status = yajl_complete_parse(msr->json->handle); if (msr->json->status != yajl_status_ok) { - if (msr->json->depth_limit_exceeded) { - *error_msg = "JSON depth limit exceeded"; - } else { - char *yajl_err = yajl_get_error(msr->json->handle, 0, NULL, 0); - *error_msg = apr_pstrdup(msr->mp, yajl_err); - yajl_free_error(msr->json->handle, yajl_err); - } + if (msr->json->depth_limit_exceeded) { + *error_msg = "JSON depth limit exceeded"; + } else { + char *yajl_err = yajl_get_error(msr->json->handle, 0, NULL, 0); + *error_msg = apr_pstrdup(msr->mp, yajl_err); + yajl_free_error(msr->json->handle, yajl_err); + } return -1; } From 9b2a5fe052b39cf6c10c0956fe0873f1cad66f86 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 1 Jan 2026 09:24:36 +0900 Subject: [PATCH 08/25] Add tests for url-encoded, JSON, and XML with ProcessPartial --- .../regression/config/10-request-directives.t | 388 +++++++++++++++++- 1 file changed, 386 insertions(+), 2 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 57be89733a..78a2093554 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -920,8 +920,392 @@ ), ), }, - - +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_name before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 12 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&bad_name=2&c=3), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_name after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 11 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&bad_name=2&c=3), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_value before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 15 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&b=bad_value&c=3), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_value after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 14 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_log => { + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&b=bad_value&c=3), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/bad_name after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 12 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + 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({"bad_name":1}), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/bad_name before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 13 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + 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({"bad_name":1}), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/bad_value after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 15 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + 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({"a":"bad_value"}), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/bad_value before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 16 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + 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({"a":"bad_value"}), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/ill-formed after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 17 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + 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({"a":"bad_value"}]), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (json/ill-formed before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 18 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"a":"bad_value"}]), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (xml/bad_value after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 11 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (xml/bad_value before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 12 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (xml/ill-formed after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 19 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (xml/ill-formed before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 20 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), +}, # SecCookieFormat { From 0bfb8283abe666dd3912038d1a12f70e411ebd2a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 1 Jan 2026 16:43:02 +0900 Subject: [PATCH 09/25] Refine tests for multipart with ProcessPartial --- .../regression/config/10-request-directives.t | 484 +++++++++--------- 1 file changed, 233 insertions(+), 251 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 78a2093554..93a132f097 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -646,79 +646,61 @@ # SecRequestBodyLimitAction ProcessPartial { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/just limit - bad_name)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/bad_name before limit)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 296 + SecRequestBodyLimit 59 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule MULTIPART_NAME "bad_name" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], - }, match_response => { status => qr/^403$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" - - value1 - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="bad_name2" - - value2 - -----------------------------69343412719991675451336310646--), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="bad_name" + ), + ) . "\r\n" . "a", + ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/greater - bad_name)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/bad_name after limit)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 295 + SecRequestBodyLimit 58 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule MULTIPART_NAME "bad_name" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], - }, match_response => { - status => qr/^403$/, + status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" - - value1 - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="bad_name2" - - value2 - -----------------------------69343412719991675451336310646--), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="bad_name" + ), + ) . "\r\n", + ), }, { type => "config", @@ -738,20 +720,20 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646--), - ), - ), + value1 + -----------------------------69343412719991675451336310646--), + ), + ), }, { type => "config", @@ -771,20 +753,20 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646--) . "\r", - ), - ), + value1 + -----------------------------69343412719991675451336310646--) . "\r", + ), + ), }, { type => "config", @@ -804,20 +786,20 @@ match_response => { status => qr/^400$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646--) . "\r", - ), - ), + value1 + -----------------------------69343412719991675451336310646--) . "\r", + ), + ), }, { type => "config", @@ -837,21 +819,21 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646-- + value1 + -----------------------------69343412719991675451336310646-- ), - ), - ), + ), + ), }, { type => "config", @@ -871,20 +853,20 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646--) . "\rbad epilogue after just CR", - ), - ), + value1 + -----------------------------69343412719991675451336310646--) . "\rbad epilogue after just CR", + ), + ), }, { type => "config", @@ -904,21 +886,21 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", - ], - normalize_raw_request_data( - q( - -----------------------------69343412719991675451336310646 - Content-Disposition: form-data; name="name1" + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=---------------------------69343412719991675451336310646", + ], + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="name1" - value1 - -----------------------------69343412719991675451336310646-- + value1 + -----------------------------69343412719991675451336310646-- ), - ), - ), + ), + ), }, { type => "config", @@ -939,15 +921,15 @@ match_response => { status => qr/^403$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/x-www-form-urlencoded", - ], - normalize_raw_request_data( - q(a=1&bad_name=2&c=3), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&bad_name=2&c=3), + ), + ), }, { type => "config", @@ -968,15 +950,15 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/x-www-form-urlencoded", - ], - normalize_raw_request_data( - q(a=1&bad_name=2&c=3), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&bad_name=2&c=3), + ), + ), }, { type => "config", @@ -997,15 +979,15 @@ match_response => { status => qr/^403$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/x-www-form-urlencoded", - ], - normalize_raw_request_data( - q(a=1&b=bad_value&c=3), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&b=bad_value&c=3), + ), + ), }, { type => "config", @@ -1026,15 +1008,15 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/x-www-form-urlencoded", - ], - normalize_raw_request_data( - q(a=1&b=bad_value&c=3), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=1&b=bad_value&c=3), + ), + ), }, { type => "config", @@ -1053,15 +1035,15 @@ 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({"bad_name":1}), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"bad_name":1}), + ), + ), }, { type => "config", @@ -1080,15 +1062,15 @@ 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({"bad_name":1}), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"bad_name":1}), + ), + ), }, { type => "config", @@ -1107,15 +1089,15 @@ 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({"a":"bad_value"}), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"a":"bad_value"}), + ), + ), }, { type => "config", @@ -1134,15 +1116,15 @@ 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({"a":"bad_value"}), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"a":"bad_value"}), + ), + ), }, { type => "config", @@ -1161,15 +1143,15 @@ 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({"a":"bad_value"}]), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"a":"bad_value"}]), + ), + ), }, { type => "config", @@ -1188,15 +1170,15 @@ match_response => { status => qr/^400$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/json", - ], - normalize_raw_request_data( - q({"a":"bad_value"}]), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + ], + normalize_raw_request_data( + q({"a":"bad_value"}]), + ), + ), }, { type => "config", @@ -1215,15 +1197,15 @@ match_response => { status => qr/^200$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/xml", - ], - normalize_raw_request_data( - q(bad_value), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), }, { type => "config", @@ -1242,15 +1224,15 @@ match_response => { status => qr/^403$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/xml", - ], - normalize_raw_request_data( - q(bad_value), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), }, { type => "config", @@ -1269,15 +1251,15 @@ match_response => { status => qr/^403$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/xml", - ], - normalize_raw_request_data( - q(bad_value), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), }, { type => "config", @@ -1296,15 +1278,15 @@ match_response => { status => qr/^400$/, }, - request => new HTTP::Request( - POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", - [ - "Content-Type" => "application/xml", - ], - normalize_raw_request_data( - q(bad_value), - ), - ), + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + ], + normalize_raw_request_data( + q(bad_value), + ), + ), }, # SecCookieFormat From 43f95fdb6623d456b091f6be703b918edb7417d9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 07:55:54 +0900 Subject: [PATCH 10/25] Modify url-encoded reqbody tests for ProcesPartial --- .../regression/config/10-request-directives.t | 219 ++++++++++++++++-- 1 file changed, 197 insertions(+), 22 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 93a132f097..a1b151b9af 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -702,6 +702,64 @@ ) . "\r\n", ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/bad_filename before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 81 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_FILENAME "bad_filename" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="bad_filename" + ), + ) . "\r\n" . "a", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/bad_filename after limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 80 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_FILENAME "bad_filename" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="bad_filename" + ), + ) . "\r\n", + ), +}, { type => "config", comment => "SecRequestBodyLimitAction ProcessPartial (multipart/no epilogue)", @@ -904,20 +962,69 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_name before limit)", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/entire/bad_name without value)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 12 + SecRequestBodyLimit 8 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(bad_name), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_name without value without delimeter before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 8 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^200$/, }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(bad_nameX), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_name without value with delimiter before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 9 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), match_response => { status => qr/^403$/, }, @@ -927,26 +1034,49 @@ "Content-Type" => "application/x-www-form-urlencoded", ], normalize_raw_request_data( - q(a=1&bad_name=2&c=3), + q(bad_name&X), ), ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_name after limit)", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/entire/bad_name with value)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 11 + SecRequestBodyLimit 10 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + match_response => { + status => qr/^403$/, }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(bad_name=1), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_name with value without delimeter before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 10 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), match_response => { status => qr/^200$/, }, @@ -956,26 +1086,23 @@ "Content-Type" => "application/x-www-form-urlencoded", ], normalize_raw_request_data( - q(a=1&bad_name=2&c=3), + q(bad_name=1X), ), ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_value before limit)", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_name with value with delimiter before limit)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 15 + SecRequestBodyLimit 11 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], - }, match_response => { status => qr/^403$/, }, @@ -985,26 +1112,49 @@ "Content-Type" => "application/x-www-form-urlencoded", ], normalize_raw_request_data( - q(a=1&b=bad_value&c=3), + q(bad_name=1&X), ), ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/bad_value after limit)", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/entire/bad_value)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 14 + SecRequestBodyLimit 11 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny ), - match_log => { - -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + match_response => { + status => qr/^403$/, }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=bad_value), + ), + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_value without delimeter before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 11 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), match_response => { status => qr/^200$/, }, @@ -1014,11 +1164,36 @@ "Content-Type" => "application/x-www-form-urlencoded", ], normalize_raw_request_data( - q(a=1&b=bad_value&c=3), + q(a=bad_valueX), ), ), }, { + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/partial/bad_value with delimeter before limit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 12 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + ], + normalize_raw_request_data( + q(a=bad_value&X), + ), + ), +},{ type => "config", comment => "SecRequestBodyLimitAction ProcessPartial (json/bad_name after limit)", conf => qq( From 3d73c02ab8c4509f5ab5cb62667d40ad7656e341 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 07:57:13 +0900 Subject: [PATCH 11/25] Support partial processing of url-encoded reqbody --- apache2/modsecurity.h | 1 + apache2/msc_parsers.c | 4 ++-- apache2/msc_reqbody.c | 4 ++++ 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index ddda27e2b9..b26e3a8f9d 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -279,6 +279,7 @@ struct modsec_rec { unsigned int if_started_forwarding; apr_size_t reqbody_length; + unsigned int reqbody_partial_proessing_enabled; apr_bucket_brigade *of_brigade; unsigned int of_status; diff --git a/apache2/msc_parsers.c b/apache2/msc_parsers.c index 793549a5f6..bbd4ba0c83 100644 --- a/apache2/msc_parsers.c +++ b/apache2/msc_parsers.c @@ -313,7 +313,7 @@ int parse_arguments(modsec_rec *msr, const char *s, apr_size_t inputlength, value = &buf[j]; } } - else { + else if (i < inputlength || msr->reqbody_partial_proessing_enabled == 0) { arg->value_len = urldecode_nonstrict_inplace_ex((unsigned char *)value, arg->value_origin_len, invalid_count, &changed); arg->value = apr_pstrmemdup(msr->mp, value, arg->value_len); @@ -330,7 +330,7 @@ int parse_arguments(modsec_rec *msr, const char *s, apr_size_t inputlength, } /* the last parameter was empty */ - if (status == 1) { + if (status == 1 && msr->reqbody_partial_proessing_enabled == 0) { arg->value_len = 0; arg->value = ""; diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 508d8ada8f..4409d2bd09 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -443,6 +443,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, * Enable partial processing of request body data. */ void modsecurity_request_body_enable_partial_processing(modsec_rec *msr) { + msr->reqbody_partial_proessing_enabled = 1; if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { msr->mpd->allow_process_partial = 1; msr_log(msr, 4, "Multipart: Allow partial processing of request body"); @@ -455,6 +456,9 @@ void modsecurity_request_body_enable_partial_processing(modsec_rec *msr) { json_allow_partial_values(msr); msr_log(msr, 4, "JSON: Allow partial processing of request body"); } + else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { + msr_log(msr, 4, "URLENCODED: Allow partial processing of request body"); + } } apr_status_t modsecurity_request_body_to_stream(modsec_rec *msr, const char *buffer, int buflen, char **error_msg) { From d914f232c6b89ff4b3ce6923ed2bf8c88a54e11a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 16:00:52 +0900 Subject: [PATCH 12/25] Add tests for MULTIPART_PART_HEADERS with ProcessPartial --- .../regression/config/10-request-directives.t | 815 ++++++++++++++++++ 1 file changed, 815 insertions(+) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index a1b151b9af..4a7cc68389 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -960,6 +960,821 @@ ), ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part across limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 114 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 115 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --000), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part across limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 115 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 116 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/parital/bad-header in part across limit #3)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 116 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 117 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000), + ) . "\rX", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 117 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 118 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000 + ) + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 118 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 119 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000 + ) + ) . q(C) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #3)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 160 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 161 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000 + ) + ) . q(Content-Disposition: form-data; name="name2) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in final part across limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 116 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 117 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000-), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in final part before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 117 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 118 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: bad_type + + value + --0000--), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in final part across limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 205 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 206 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: text/plain + + value + --0000 + Content-Disposition: form-data; name="name2" + Content-Type: bad_type + + value + --0000-), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in final part before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 206 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 207 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: text/plain + + value + --0000 + Content-Disposition: form-data; name="name2" + Content-Type: bad_type + + value + --0000--), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid final boundary before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 118 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 119 bytes./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: text/plain + + value + --0000!), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid final boundary before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 119 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 120 bytes./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: text/plain + + value + --0000-!), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in part across limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 109 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 110 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--000), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in part across limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 110 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 111 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 111 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 112 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000), + ) . "\n" . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 112 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 113 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000), + q(C), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #3)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 154 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS:name1 "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 155 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000), + q(Content-Disposition: form-data; name="name2), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in final part across limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 111 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 112 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000-), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in final part before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 112 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 113 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000--), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in final part across limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 195 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 196 bytes./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: text/plain), + q(), + q(value), + q(--0000), + q(Content-Disposition: form-data; name="name2"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000-), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in final part before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 196 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule MULTIPART_PART_HEADERS "content-type:.*bad_type" "id:'200002',phase:2,t:none,t:lowercase,deny + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 197 bytes./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: text/plain), + q(), + q(value), + q(--0000), + q(Content-Disposition: form-data; name="name2"), + q(Content-Type: bad_type), + q(), + q(value), + q(--0000--), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid final boundary before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 113 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 114 bytes./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: text/plain), + q(), + q(value), + q(--0000!), + ) . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid final boundary before limit #2)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 114 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 115 bytes./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + join("\n", + q(--0000), + q(Content-Disposition: form-data; name="name1"; filename="name1.txt"), + q(Content-Type: text/plain), + q(), + q(value), + q(--0000-!), + ) . "X", + ), +}, { type => "config", comment => "SecRequestBodyLimitAction ProcessPartial (url-encoded/entire/bad_name without value)", From 55a1828331687be089e2d7ba75fd9b359126c5a1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 16:55:00 +0900 Subject: [PATCH 13/25] Reject invalid final boundary for multipart with ProcessPartial --- apache2/msc_multipart.c | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 2ffaf052a5..708d8d52a4 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1083,6 +1083,22 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { /* The payload is complete after all. */ msr->mpd->is_complete = 1; } + else if (msr->mpd->allow_process_partial == 1 + && (buf_data_len >= 2 + strlen(msr->mpd->boundary)) + && (*(msr->mpd->buf) == '-') + && (*(msr->mpd->buf + 1) == '-') + && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) ) + { + if ( ((buf_data_len >= 3 + strlen(msr->mpd->boundary)) + && ((*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) != '-') + && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) != '\r'))) + || ((buf_data_len >= final_boundary_len) + && *(msr->mpd->buf + 2 + strlen(msr->mpd->boundary) + 1) != '-') ) + { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid final boundary."); + return -1; + } + } } if (msr->mpd->is_complete == 0 && msr->mpd->allow_process_partial == 0) { From 0b599ad073d69cff3f67ab7474f3b13c0a9f5e0b Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 16:57:16 +0900 Subject: [PATCH 14/25] Fix indent in apache2/msc_multipart.c --- apache2/msc_multipart.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 708d8d52a4..b7f4b85af9 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -421,7 +421,7 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { if (data == msr->mpd->buf) { *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid part header (header name missing)."); - return -1; + return -1; } /* check if multipart header contains any invalid characters */ From cb95a24efda2353f60eda880837bdd13fc6aa8bb Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 22:28:03 +0900 Subject: [PATCH 15/25] Modify tests for multipart with ProcessPartial --- .../regression/config/10-request-directives.t | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 4a7cc68389..2019b0f20a 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -997,7 +997,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part across limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #1)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1012,7 +1012,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 116 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1032,7 +1032,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/parital/bad-header in part across limit #3)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/parital/bad-header in part before limit #2)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1047,7 +1047,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 117 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1067,7 +1067,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #1)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #3)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1103,7 +1103,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #4)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1139,7 +1139,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #3)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/bad-header in part before limit #5)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1190,7 +1190,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 117 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1260,7 +1260,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 206 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1325,7 +1325,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid final boundary before limit #1)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid boundary before limit #1)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1337,6 +1337,7 @@ ), match_log => { debug => [ qr/Input filter: Bucket type HEAP contains 119 bytes./, 1], + error => [ qr/Multipart parsing error: Multipart: Invalid boundary./, 1], }, match_response => { status => qr/^400$/, @@ -1353,13 +1354,13 @@ Content-Type: text/plain value - --0000!), + --0000!) ) . "X", ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid final boundary before limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid boundary before limit #2)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1371,6 +1372,42 @@ ), match_log => { debug => [ qr/Input filter: Bucket type HEAP contains 120 bytes./, 1], + error => [ qr/Multipart parsing error: Multipart: Invalid boundary./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "multipart/form-data; boundary=0000", + ], + normalize_raw_request_data( + q( + --0000 + Content-Disposition: form-data; name="name1"; filename="name1.txt" + Content-Type: text/plain + + value + --0000) + ) . "\r!" . "X", + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/CRLF/partial/invalid final boundary before limit #1)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 119 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + ), + match_log => { + debug => [ qr/Input filter: Bucket type HEAP contains 120 bytes./, 1], + error => [ qr/Multipart parsing error: Multipart: Invalid final boundary./, 1], }, match_response => { status => qr/^400$/, @@ -1427,7 +1464,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in part across limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/bad-header in part before limit #1)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1442,7 +1479,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 111 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1461,7 +1498,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #1)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #2)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1495,7 +1532,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #3)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1530,7 +1567,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #3)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/parital/bad-header in part before limit #4)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1580,7 +1617,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 112 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1648,7 +1685,7 @@ debug => [ qr/Input filter: Bucket type HEAP contains 196 bytes./, 1], }, match_response => { - status => qr/^200$/, + status => qr/^403$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -1711,7 +1748,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid final boundary before limit #1)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid boundary before limit #1)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1723,6 +1760,7 @@ ), match_log => { debug => [ qr/Input filter: Bucket type HEAP contains 114 bytes./, 1], + error => [ qr/Multipart parsing error: Multipart: Invalid boundary./, 1], }, match_response => { status => qr/^400$/, @@ -1744,7 +1782,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid final boundary before limit #2)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart/LF/partial/invalid final boundary before limit #1)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -1756,6 +1794,7 @@ ), match_log => { debug => [ qr/Input filter: Bucket type HEAP contains 115 bytes./, 1], + error => [ qr/Multipart parsing error: Multipart: Invalid final boundary./, 1], }, match_response => { status => qr/^400$/, From 61d4e4558c276f9f1029b0c8672c6fd6e3a29932 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 22:28:24 +0900 Subject: [PATCH 16/25] Modify multipart_complete for incomplete final boundary --- apache2/msc_multipart.c | 101 ++++++++++++++++++++++------------------ 1 file changed, 56 insertions(+), 45 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index b7f4b85af9..68f9ca68b2 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1044,59 +1044,70 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { * [CRLF epilogue] */ unsigned int buf_data_len = (unsigned int)(MULTIPART_BUF_SIZE - msr->mpd->bufleft); - size_t final_boundary_len = 4 + strlen(msr->mpd->boundary); - if ( (buf_data_len >= final_boundary_len) + size_t boundary_len = strlen(msr->mpd->boundary); + if ( (buf_data_len >= 2 + boundary_len) && (*(msr->mpd->buf) == '-') && (*(msr->mpd->buf + 1) == '-') - && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) - && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) == '-') - && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary) + 1) == '-') ) + && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, boundary_len) == 0) ) { - /* If body fits in limit and ends with final boundary plus just CR, reject it. */ - if ( (msr->mpd->allow_process_partial == 0) - && (buf_data_len == final_boundary_len + 1) - && (*(msr->mpd->buf + final_boundary_len) == '\r') ) + if ( (buf_data_len >= 2 + boundary_len + 2) + && (*(msr->mpd->buf + 2 + boundary_len) == '-') + && (*(msr->mpd->buf + 2 + boundary_len + 1) == '-') ) { - *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid epilogue after final boundary."); - return -1; - } + /* If body fits in limit and ends with final boundary plus just CR, reject it. */ + if ( (msr->mpd->allow_process_partial == 0) + && (buf_data_len == 2 + boundary_len + 2 + 1) + && (*(msr->mpd->buf + 2 + boundary_len + 2) == '\r') ) + { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid epilogue after final boundary."); + return -1; + } - if ((msr->mpd->crlf_state_buf_end == 2) && (msr->mpd->flag_lf_line != 1)) { - msr->mpd->flag_lf_line = 1; - if (msr->mpd->flag_crlf_line) { - msr_log(msr, 4, "Multipart: Warning: mixed line endings used (CRLF/LF)."); - } else { - msr_log(msr, 4, "Multipart: Warning: incorrect line endings used (LF)."); + if ((msr->mpd->crlf_state_buf_end == 2) && (msr->mpd->flag_lf_line != 1)) { + msr->mpd->flag_lf_line = 1; + if (msr->mpd->flag_crlf_line) { + msr_log(msr, 4, "Multipart: Warning: mixed line endings used (CRLF/LF)."); + } else { + msr_log(msr, 4, "Multipart: Warning: incorrect line endings used (LF)."); + } } + if (msr->mpd->mpp_substate_part_data_read == 0) { + /* it looks like the final boundary, but it's where part data should begin */ + msr->mpd->flag_invalid_part = 1; + msr_log(msr, 4, "Multipart: Warning: Invalid part (data contains final boundary)"); + } + /* Looks like the final boundary - process it. */ + if (multipart_process_boundary(msr, 1 /* final */, error_msg) < 0) { + msr->mpd->flag_error = 1; + return -1; + } + + /* The payload is complete after all. */ + msr->mpd->is_complete = 1; } - if (msr->mpd->mpp_substate_part_data_read == 0) { - /* it looks like the final boundary, but it's where part data should begin */ - msr->mpd->flag_invalid_part = 1; - msr_log(msr, 4, "Multipart: Warning: Invalid part (data contains final boundary)"); - } - /* Looks like the final boundary - process it. */ - if (multipart_process_boundary(msr, 1 /* final */, error_msg) < 0) { - msr->mpd->flag_error = 1; - return -1; - } + else if (msr->mpd->allow_process_partial == 1) { + int is_final = 0; + if (buf_data_len >= 2 + boundary_len + 1) { + if (*(msr->mpd->buf + 2 + boundary_len) == '-') { + if ( (buf_data_len >= 2 + boundary_len + 2) + && (*(msr->mpd->buf + 2 + boundary_len + 1) != '-') ) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid final boundary."); + return -1; + } + is_final = 1; + } + else if ( (*(msr->mpd->buf + 2 + boundary_len) != '\r') + || ((buf_data_len >= 2 + boundary_len + 2) + && (*(msr->mpd->buf + 2 + boundary_len + 1) != '\n')) ) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid boundary."); + return -1; + } + } - /* The payload is complete after all. */ - msr->mpd->is_complete = 1; - } - else if (msr->mpd->allow_process_partial == 1 - && (buf_data_len >= 2 + strlen(msr->mpd->boundary)) - && (*(msr->mpd->buf) == '-') - && (*(msr->mpd->buf + 1) == '-') - && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) ) - { - if ( ((buf_data_len >= 3 + strlen(msr->mpd->boundary)) - && ((*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) != '-') - && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) != '\r'))) - || ((buf_data_len >= final_boundary_len) - && *(msr->mpd->buf + 2 + strlen(msr->mpd->boundary) + 1) != '-') ) - { - *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid final boundary."); - return -1; + if (multipart_process_boundary(msr, is_final, error_msg) < 0) { + msr->mpd->flag_error = 1; + return -1; + } } } } From be5feeed1d02bcdb851f0af222fdc8ff9a29e8e9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Sat, 3 Jan 2026 23:51:59 +0900 Subject: [PATCH 17/25] Process an incomplete boundary as a non-final boundary --- apache2/msc_multipart.c | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 68f9ca68b2..184c5d2c30 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -1086,7 +1086,6 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { msr->mpd->is_complete = 1; } else if (msr->mpd->allow_process_partial == 1) { - int is_final = 0; if (buf_data_len >= 2 + boundary_len + 1) { if (*(msr->mpd->buf + 2 + boundary_len) == '-') { if ( (buf_data_len >= 2 + boundary_len + 2) @@ -1094,7 +1093,6 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid final boundary."); return -1; } - is_final = 1; } else if ( (*(msr->mpd->buf + 2 + boundary_len) != '\r') || ((buf_data_len >= 2 + boundary_len + 2) @@ -1103,8 +1101,8 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { return -1; } } - - if (multipart_process_boundary(msr, is_final, error_msg) < 0) { + /* process it as a non-final boundary to avoid building a new part. */ + if (multipart_process_boundary(msr, 0, error_msg) < 0) { msr->mpd->flag_error = 1; return -1; } From 50de8bbdc0839771ed196918d358ce88f53c92a3 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 11:31:01 +0900 Subject: [PATCH 18/25] Update tests/regression/rule/15-json.t Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/regression/rule/15-json.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 1e1c93804f..63081b6a78 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -274,7 +274,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], From a64b54436ce2efccd4ef5cf0dd3e403be29e9939 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 11:32:01 +0900 Subject: [PATCH 19/25] Update tests/regression/rule/15-json.t Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/regression/rule/15-json.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 63081b6a78..c7fe119533 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -307,7 +307,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], From a83c26c97977368cfdcbe671126b945dc301640a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 11:36:02 +0900 Subject: [PATCH 20/25] Fix spelling of reqbody_partial_processing_enabled --- apache2/modsecurity.h | 2 +- apache2/msc_parsers.c | 4 ++-- apache2/msc_reqbody.c | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index b26e3a8f9d..6f94a856f8 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -279,7 +279,7 @@ struct modsec_rec { unsigned int if_started_forwarding; apr_size_t reqbody_length; - unsigned int reqbody_partial_proessing_enabled; + unsigned int reqbody_partial_processing_enabled; apr_bucket_brigade *of_brigade; unsigned int of_status; diff --git a/apache2/msc_parsers.c b/apache2/msc_parsers.c index bbd4ba0c83..30234c8d89 100644 --- a/apache2/msc_parsers.c +++ b/apache2/msc_parsers.c @@ -313,7 +313,7 @@ int parse_arguments(modsec_rec *msr, const char *s, apr_size_t inputlength, value = &buf[j]; } } - else if (i < inputlength || msr->reqbody_partial_proessing_enabled == 0) { + else if (i < inputlength || msr->reqbody_partial_processing_enabled == 0) { arg->value_len = urldecode_nonstrict_inplace_ex((unsigned char *)value, arg->value_origin_len, invalid_count, &changed); arg->value = apr_pstrmemdup(msr->mp, value, arg->value_len); @@ -330,7 +330,7 @@ int parse_arguments(modsec_rec *msr, const char *s, apr_size_t inputlength, } /* the last parameter was empty */ - if (status == 1 && msr->reqbody_partial_proessing_enabled == 0) { + if (status == 1 && msr->reqbody_partial_processing_enabled == 0) { arg->value_len = 0; arg->value = ""; diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 4409d2bd09..9f273c0424 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -443,7 +443,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, * Enable partial processing of request body data. */ void modsecurity_request_body_enable_partial_processing(modsec_rec *msr) { - msr->reqbody_partial_proessing_enabled = 1; + msr->reqbody_partial_processing_enabled = 1; if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { msr->mpd->allow_process_partial = 1; msr_log(msr, 4, "Multipart: Allow partial processing of request body"); From da8b174ac1253d9e2cfd1143987b369930632096 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 11:44:49 +0900 Subject: [PATCH 21/25] Fix indentations in test files --- tests/regression/rule/10-xml.t | 4 ++-- tests/regression/rule/15-json.t | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/regression/rule/10-xml.t b/tests/regression/rule/10-xml.t index 0a5e35eb27..5edf1ea904 100644 --- a/tests/regression/rule/10-xml.t +++ b/tests/regression/rule/10-xml.t @@ -445,7 +445,7 @@ SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], + error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], }, match_response => { status => qr/^403$/, @@ -477,7 +477,7 @@ SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], + error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], }, match_response => { status => qr/^403$/, diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index c7fe119533..5c19a382ba 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -339,7 +339,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { debug => [ qr/JSON: Allow partial processing of request body|JSON support was not enabled/, 1 ], @@ -370,7 +370,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Access denied with code 403 \(phase 2\)\. Pattern match "bad_value" at ARGS:b\./, 1 ], @@ -403,7 +403,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(26\)\./, 1 ], @@ -434,7 +434,7 @@ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" SecRule REQBODY_ERROR "!\@eq 0" \\ "id:'200002', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" - SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(26\)\./, 1 ], From f679e11d3238d78a71ce3b34f35af00699105603 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 13:24:53 +0900 Subject: [PATCH 22/25] Add rules to check multipart error in tests --- .../regression/config/10-request-directives.t | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 2019b0f20a..91596efcb3 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -499,6 +499,23 @@ SecRequestBodyAccess On SecRequestBodyLimitAction Reject SecRequestBodyLimit 20 + SecRule MULTIPART_STRICT_ERROR "!\@eq 0" \\ + "id:'200003',phase:2,t:none,log,deny,status:400, \\ + msg:'Multipart request body failed strict validation: \\ + PE %{REQBODY_PROCESSOR_ERROR}, \\ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ + DB %{MULTIPART_DATA_BEFORE}, \\ + DA %{MULTIPART_DATA_AFTER}, \\ + HF %{MULTIPART_HEADER_FOLDING}, \\ + LF %{MULTIPART_LF_LINE}, \\ + SM %{MULTIPART_MISSING_SEMICOLON}, \\ + IQ %{MULTIPART_INVALID_QUOTING}, \\ + IP %{MULTIPART_INVALID_PART}, \\ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + SecRule MULTIPART_UNMATCHED_BOUNDARY "!\@eq 0" \\ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" ), match_log => { debug => [ qr/Request body is larger than the configured limit \(20\)./, 1 ], @@ -576,6 +593,23 @@ SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial SecRequestBodyLimit 131072 + SecRule MULTIPART_STRICT_ERROR "!\@eq 0" \\ + "id:'200003',phase:2,t:none,log,deny,status:400, \\ + msg:'Multipart request body failed strict validation: \\ + PE %{REQBODY_PROCESSOR_ERROR}, \\ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ + DB %{MULTIPART_DATA_BEFORE}, \\ + DA %{MULTIPART_DATA_AFTER}, \\ + HF %{MULTIPART_HEADER_FOLDING}, \\ + LF %{MULTIPART_LF_LINE}, \\ + SM %{MULTIPART_MISSING_SEMICOLON}, \\ + IQ %{MULTIPART_INVALID_QUOTING}, \\ + IP %{MULTIPART_INVALID_PART}, \\ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + SecRule MULTIPART_UNMATCHED_BOUNDARY "!\@eq 0" \\ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" ), match_log => { -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], From 927758908e9ce1196e2abc51fa461b78f8ff2475 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 14:01:34 +0900 Subject: [PATCH 23/25] Fix indentations in tests/regression/rule/10-xml.t --- tests/regression/rule/10-xml.t | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/tests/regression/rule/10-xml.t b/tests/regression/rule/10-xml.t index 5edf1ea904..673651226a 100644 --- a/tests/regression/rule/10-xml.t +++ b/tests/regression/rule/10-xml.t @@ -8,7 +8,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500005, \\ @@ -56,7 +56,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecAuditEngine RelevantOnly @@ -106,7 +106,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecAuditEngine RelevantOnly @@ -157,7 +157,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecAuditEngine RelevantOnly @@ -208,7 +208,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecAuditEngine RelevantOnly @@ -259,7 +259,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500020, \\ @@ -303,7 +303,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRule REQUEST_HEADERS:Content-Type "^text/xml\$" "id:500023, \\ @@ -347,7 +347,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecAuditEngine RelevantOnly @@ -393,7 +393,7 @@ conf => qq( SecRuleEngine On SecRequestBodyAccess On - SecXmlExternalEntity On + SecXmlExternalEntity On SecDebugLog $ENV{DEBUG_LOG} SecDebugLogLevel 9 SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" "id:500029, \\ @@ -477,7 +477,7 @@ SecRule XML:/* "bad_value" "id:'500007',phase:2,t:none,deny" ), match_log => { - error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], + error => [ qr/Access denied with code 403 \(phase 2\). Pattern match "bad_value" at XML\./, 1 ], }, match_response => { status => qr/^403$/, From 96a8326ebdc6f1a71dd16289848c4d310290da6c Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 23 Jan 2026 14:44:07 +0900 Subject: [PATCH 24/25] Make handling of SecRequestBodyNoFilesLimit consistent between modsecurity_request_body_store and modsecurity_request_body_store. --- apache2/apache2_io.c | 25 ++++++------------------- apache2/msc_reqbody.c | 18 +++++++++++------- 2 files changed, 17 insertions(+), 26 deletions(-) diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c index 073fefe128..a350e4d1bf 100644 --- a/apache2/apache2_io.c +++ b/apache2/apache2_io.c @@ -311,27 +311,13 @@ apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { int rcbs = modsecurity_request_body_store(msr, buf, buflen, error_msg); if (rcbs < 0) { if (rcbs == -5) { - 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; - } 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); - } else if ((msr->txcfg->is_enabled == MODSEC_DETECTION_ONLY) && (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); - } 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 HTTP_REQUEST_ENTITY_TOO_LARGE; } - if((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) + if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { return HTTP_INTERNAL_SERVER_ERROR; + } } - } if (APR_BUCKET_IS_EOS(bucket)) { @@ -352,11 +338,12 @@ apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { msr->if_status = IF_STATUS_WANTS_TO_RUN; - if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) { + if (rcbe < 0) { if (rcbe == -5) { return HTTP_REQUEST_ENTITY_TOO_LARGE; } - if (rcbe < 0) { + + if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { return HTTP_INTERNAL_SERVER_ERROR; } } diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 9f273c0424..8c2d0d14e5 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -406,21 +406,19 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } /* Check that we are not over the request body no files limit. */ - if (msr->msc_reqbody_no_files_length > (unsigned long) msr->txcfg->reqbody_no_files_limit) { + if (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) { *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); + "configured limit (%ld).", msr->txcfg->reqbody_no_files_limit); if (msr->txcfg->debuglog_level >= 1) { msr_log(msr, 1, "%s", *error_msg); } - if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) { msr->msc_reqbody_error = 1; + } if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { return -5; - } else if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { - if (msr->txcfg->is_enabled == MODSEC_ENABLED) - return -5; } } @@ -701,7 +699,13 @@ apr_status_t modsecurity_request_body_end(modsec_rec *msr, char **error_msg) { msr_log(msr, 1, "%s", *error_msg); } - return -5; + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT) { + msr->msc_reqbody_error = 1; + } + + if ((msr->txcfg->is_enabled == MODSEC_ENABLED) && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_REJECT)) { + return -5; + } } From 2681c706b81688aeed7719b9265f66290505c87a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 30 Jan 2026 16:43:27 +0900 Subject: [PATCH 25/25] Add tests for long body --- .../regression/config/10-request-directives.t | 497 +++++++++++++++++- 1 file changed, 495 insertions(+), 2 deletions(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index 91596efcb3..f76ef95d83 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -1857,10 +1857,14 @@ SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 8 + SecRequestBodyNoFilesLimit 15 + SecRequestBodyLimit 16 SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(15\)\./, 1 ], + }, match_response => { status => qr/^403$/, }, @@ -1870,7 +1874,7 @@ "Content-Type" => "application/x-www-form-urlencoded", ], normalize_raw_request_data( - q(bad_name), + q(a=1&b=2&bad_name), ), ), }, @@ -2494,3 +2498,492 @@ "a=0123456789ABCDE", ), }, +# "long-body" means that we have multiple buckets in input filter brigade +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (url-encoded/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8208 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "8209", + ], + 'a=1&b=' . 'b' x 8192 . '&bad_name&c', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (url-encoded/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8208 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "8209", + ], + 'a=1&b=' . 'b' x 8193 . '&bad_name&', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (url-encoded/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8208 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "8208", + ], + 'a=1&b=' . 'b' x 8193 . '&bad_name', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (url-encoded/long-bodyNoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8200 + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "8200", + ], + 'a=1&b=' . 'b' x 8193 . '&', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (json/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8219 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "8220", + ], + '{"a":1,"b":"' . 'b' x 8192 . '","bad_name":1, ', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (json/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8219 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "8220", + ], + '{"a":1,"b":"' . 'b' x 8192 . '", "bad_name": 1', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (json/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8219 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "8192", + ], + '{"a":1,"b":"' . 'b' x 8192 . '","bad_name":1,', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (xml/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8214 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + "Content-Length" => "8215", + ], + '' . 'b' x 8192 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (xml/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8214 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + "Content-Length" => "8215", + ], + '' . 'b' x 8192 . ' bad_value', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (xml/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8214 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200001', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Request body no files data length is larger than the configured limit \(2048\)\./, 1 ], + }, + match_response => { + status => qr/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/xml", + "Content-Length" => "8214", + ], + '' . 'b' x 8192 . 'bad_value', + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (multipart/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8472 + SecRule MULTIPART_STRICT_ERROR "!\@eq 0" \\ + "id:'200003',phase:2,t:none,log,deny,status:400, \\ + msg:'Multipart request body failed strict validation: \\ + PE %{REQBODY_PROCESSOR_ERROR}, \\ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ + DB %{MULTIPART_DATA_BEFORE}, \\ + DA %{MULTIPART_DATA_AFTER}, \\ + HF %{MULTIPART_HEADER_FOLDING}, \\ + LF %{MULTIPART_LF_LINE}, \\ + SM %{MULTIPART_MISSING_SEMICOLON}, \\ + IQ %{MULTIPART_INVALID_QUOTING}, \\ + IP %{MULTIPART_INVALID_PART}, \\ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + SecRule MULTIPART_UNMATCHED_BOUNDARY "!\@eq 0" \\ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200005', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200006',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Multipart: Allow partial processing of request body/, 1 ], + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^403$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1) . "a" x 8192 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + bad_value + -----------------------------69343412719991675451336310646) + ) . "\r", + 8192 + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (multipart/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8471 + SecRule MULTIPART_STRICT_ERROR "!\@eq 0" \\ + "id:'200003',phase:2,t:none,log,deny,status:400, \\ + msg:'Multipart request body failed strict validation: \\ + PE %{REQBODY_PROCESSOR_ERROR}, \\ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ + DB %{MULTIPART_DATA_BEFORE}, \\ + DA %{MULTIPART_DATA_AFTER}, \\ + HF %{MULTIPART_HEADER_FOLDING}, \\ + LF %{MULTIPART_LF_LINE}, \\ + SM %{MULTIPART_MISSING_SEMICOLON}, \\ + IQ %{MULTIPART_INVALID_QUOTING}, \\ + IP %{MULTIPART_INVALID_PART}, \\ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + SecRule MULTIPART_UNMATCHED_BOUNDARY "!\@eq 0" \\ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200005', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200006',phase:2,t:none,deny + ), + match_log => { + debug => [ qr/Multipart: Allow partial processing of request body/, 1 ], + -error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^200$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1) . "a" x 8192 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + bad_value + -----------------------------69343412719991675451336310646) + ), + 8192 + ), +}, +{ + type => "config", + comment => "ProcessPartial NoFilesLimit (multipart/long-body/NoFilesLimit qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 2048 + SecRequestBodyLimit 8472 + SecRule MULTIPART_STRICT_ERROR "!\@eq 0" \\ + "id:'200003',phase:2,t:none,log,deny,status:400, \\ + msg:'Multipart request body failed strict validation: \\ + PE %{REQBODY_PROCESSOR_ERROR}, \\ + BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ + BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ + DB %{MULTIPART_DATA_BEFORE}, \\ + DA %{MULTIPART_DATA_AFTER}, \\ + HF %{MULTIPART_HEADER_FOLDING}, \\ + LF %{MULTIPART_LF_LINE}, \\ + SM %{MULTIPART_MISSING_SEMICOLON}, \\ + IQ %{MULTIPART_INVALID_QUOTING}, \\ + IP %{MULTIPART_INVALID_PART}, \\ + IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ + FL %{MULTIPART_FILE_LIMIT_EXCEEDED}'" + SecRule MULTIPART_UNMATCHED_BOUNDARY "!\@eq 0" \\ + "id:'200004',phase:2,t:none,log,deny,msg:'Multipart parser detected a possible unmatched boundary.'" + SecRule REQBODY_ERROR "!\@eq 0" "id:'200005', phase:2,t:none,log,deny,status:400,msg:'Failed to parse request body.',logdata:'%{reqbody_error_msg}',severity:2" + SecRule ARGS "bad_value" "id:'200006',phase:2,t:none,deny + ), + match_log => { + -debug => [ qr/Multipart: Allow partial processing of request body/, 1 ], + error => [ qr/Multipart parsing error: Multipart: Final boundary missing./, 1], + }, + match_response => { + status => qr/^400$/, + }, + request => normalize_raw_request_data( + qq( + POST /test.txt HTTP/1.1 + Host: $ENV{SERVER_NAME}:$ENV{SERVER_PORT} + User-Agent: $ENV{USER_AGENT} + Content-Type: multipart/form-data; boundary=---------------------------69343412719991675451336310646 + Transfer-Encoding: chunked + + ), + ) + .encode_chunked( + normalize_raw_request_data( + q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="a" + + 1) . "a" x 8192 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + bad_value + -----------------------------69343412719991675451336310646) + ), + 8192 + ), +},