From a682f13270e0f8099db77e2abc7cf893e03624b9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 25 Dec 2025 22:14:43 +0900 Subject: [PATCH 01/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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/41] 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 + ), +}, From b2072485827418dc1a3ae7700022a03a8a776ee0 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 17 Mar 2026 16:34:49 +0900 Subject: [PATCH 26/41] Fix content-length in a regression test case --- tests/regression/config/10-request-directives.t | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index f76ef95d83..a2eeaba23d 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -2700,7 +2700,7 @@ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ "Content-Type" => "application/json", - "Content-Length" => "8192", + "Content-Length" => "8219", ], '{"a":1,"b":"' . 'b' x 8192 . '","bad_name":1,', ), From 72045a480ed65672a77da858ce7ee98737d0dfd9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 17 Mar 2026 17:36:18 +0900 Subject: [PATCH 27/41] Add test cases for SecRequestBodyLimitAction --- .../10-reqbody-limit-action-forcebodybuf.t | 192 +++++++++++ .../config/10-reqbody-limit-action-json.t | 196 +++++++++++ ...0-reqbody-limit-action-multipart-chunked.t | 318 ++++++++++++++++++ .../10-reqbody-limit-action-urlencoded.t | 189 +++++++++++ .../config/10-reqbody-limit-action-xml.t | 196 +++++++++++ tests/run-regression-tests.pl.in | 7 + 6 files changed, 1098 insertions(+) create mode 100644 tests/regression/config/10-reqbody-limit-action-forcebodybuf.t create mode 100644 tests/regression/config/10-reqbody-limit-action-json.t create mode 100644 tests/regression/config/10-reqbody-limit-action-multipart-chunked.t create mode 100644 tests/regression/config/10-reqbody-limit-action-urlencoded.t create mode 100644 tests/regression/config/10-reqbody-limit-action-xml.t diff --git a/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t new file mode 100644 index 0000000000..8d07319653 --- /dev/null +++ b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t @@ -0,0 +1,192 @@ +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (forcebodybuf, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/plain", + "Content-Length" => "16384", + ], + "a" x 16384, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (forcebodybuf, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/plain", + "Content-Length" => "16385", + ], + "a" x 16385, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (forcebodybuf, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "16385", + ], + "a" x 16385, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (forcebodybuf, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "32769", + ], + "a" x 32769, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/plain", + "Content-Length" => "16385", + ], + "a" x 16385, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, <=NoFilesLimit) should be 200", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/exit signal Segmentation fault \(11\), possible coredump in/, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "text/plain", + "Content-Length" => "16385", + ], + "a" x 16385, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, >NoFilesLimit) should be 200", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" + ), + match_log => { + error => [ qr/exit signal Segmentation fault \(11\), possible coredump in/, 1 ], + }, + match_response => { + status => qr/^500$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "32769", + ], + "a" x 32769, + ), +}, diff --git a/tests/regression/config/10-reqbody-limit-action-json.t b/tests/regression/config/10-reqbody-limit-action-json.t new file mode 100644 index 0000000000..55dbdb0eca --- /dev/null +++ b/tests/regression/config/10-reqbody-limit-action-json.t @@ -0,0 +1,196 @@ +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . "1" x 16376 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "16385", + ], + '{"a":"' . "1" x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "16385", + ], + '{"a":"' . "1" x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "32769", + ], + '{"a":"' . "1" x 32761 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . "1" x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . "1" x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 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" => "32769", + ], + '{"a":"' . "1" x 32761 . '"}', + ), +}, diff --git a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t new file mode 100644 index 0000000000..b84bbb2d4c --- /dev/null +++ b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t @@ -0,0 +1,318 @@ +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, <=NoFilesLimit)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200003',phase:2,t:none,log, \\ + msg:'Multipart flags: \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + ) + ), + match_log => { + error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16278 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, >NoFilesLimit)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200003',phase:2,t:none,log, \\ + msg:'Multipart flags: \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + ) + ), + match_log => { + -error => [ qr/Multipart flags:/, 1], + }, + match_response => { + status => qr/^413$/, + }, + 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16279 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, >Limit)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200003',phase:2,t:none,log, \\ + msg:'Multipart flags: \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + ) + ), + match_log => { + -error => [ qr/Multipart flags:/, 1], + }, + match_response => { + status => qr/^413$/, + }, + 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16278 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >NoFilesLimit)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200003',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + ), + 4096 + ), + match_log => { + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16279 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >Limit)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200003',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16278 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, diff --git a/tests/regression/config/10-reqbody-limit-action-urlencoded.t b/tests/regression/config/10-reqbody-limit-action-urlencoded.t new file mode 100644 index 0000000000..85f823ad99 --- /dev/null +++ b/tests/regression/config/10-reqbody-limit-action-urlencoded.t @@ -0,0 +1,189 @@ +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + "a=1&b=" . "2" x 16378, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "16385", + ], + "a=1&b=" . "2" x 16379, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "16385", + ], + "a=1&b=" . "2" x 16379, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "32769", + ], + "a=1&b=" . "2" x 32763, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + "a=1&b=" . "2" x 16379, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + "a=1&b=" . "2" x 16379, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 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" => "32769", + ], + "a=1&b=" . "2" x 32763, + ), +}, \ No newline at end of file diff --git a/tests/regression/config/10-reqbody-limit-action-xml.t b/tests/regression/config/10-reqbody-limit-action-xml.t new file mode 100644 index 0000000000..4cccd6b143 --- /dev/null +++ b/tests/regression/config/10-reqbody-limit-action-xml.t @@ -0,0 +1,196 @@ +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (XML, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . "1" x 16364 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (XML, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . "1" x 16365 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (XML, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . "1" x 16365 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (XML, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + }, + match_response => { + status => qr/^413$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "32769", + ], + '' . "1" x 32749 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . "1" x 16365 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, <=NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . "1" x 16365 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, >NoFilesLimit)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "32769", + ], + '' . "1" x 32749 . '', + ), +}, diff --git a/tests/run-regression-tests.pl.in b/tests/run-regression-tests.pl.in index 87b1933748..c84ad724ed 100755 --- a/tests/run-regression-tests.pl.in +++ b/tests/run-regression-tests.pl.in @@ -354,6 +354,13 @@ sub runfile { msg(sprintf("Passed: %2d; Failed: %2d", $pass, $testnum ? (1 - $pass) : ($n - $pass))); } +# Trim indent in action strings +sub trim_action_indent { + my $r = $_[0]; + $r =~ s/[\t]+//mg; + return $r; +} + # Take out any indenting and translate LF -> CRLF sub normalize_raw_request_data { my $r = $_[0]; From a7d1198a1756e39b57df198e09828308325689a8 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 18 Mar 2026 08:42:18 +0900 Subject: [PATCH 28/41] Fix segmentation fault in modsecurity_request_body_enable_partial_processing --- apache2/msc_reqbody.c | 5 ++++- .../config/10-reqbody-limit-action-forcebodybuf.t | 14 ++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 8c2d0d14e5..f660c24e40 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -442,7 +442,10 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, */ void modsecurity_request_body_enable_partial_processing(modsec_rec *msr) { msr->reqbody_partial_processing_enabled = 1; - if (strcmp(msr->msc_reqbody_processor, "MULTIPART") == 0) { + if (msr->msc_reqbody_processor == NULL) { + msr_log(msr, 9, "enable_partial_processing for none reqbody_processor"); + } + else 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"); } diff --git a/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t index 8d07319653..e25794210a 100644 --- a/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t +++ b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t @@ -136,7 +136,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, <=NoFilesLimit) should be 200", + comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, <=NoFilesLimit)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -148,10 +148,11 @@ SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" ), match_log => { - error => [ qr/exit signal Segmentation fault \(11\), possible coredump in/, 1 ], + error => [ qr/ModSecurity: Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + debug => [ qr/enable_partial_processing for none reqbody_processor/, 1 ], }, match_response => { - status => qr/^500$/, + status => qr/^200$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", @@ -164,7 +165,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, >NoFilesLimit) should be 200", + comment => "SecRequestBodyLimitAction ProcessPartial (forcebodybuf, >Limit, >NoFilesLimit)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -176,10 +177,11 @@ SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" ), match_log => { - error => [ qr/exit signal Segmentation fault \(11\), possible coredump in/, 1 ], + error => [ qr/ModSecurity: Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], + debug => [ qr/enable_partial_processing for none reqbody_processor/, 1 ], }, match_response => { - status => qr/^500$/, + status => qr/^200$/, }, request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", From 8ccfc6a6dea2ee2cdcf103c17a79db4d76139921 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 26 Mar 2026 09:27:45 +0900 Subject: [PATCH 29/41] Fix request body partial processing for XML Pass 0 (false) as the terminate argument of the last call of xmlParseChunk when partial processing is enabled and the length exceeds the limit. --- apache2/msc_xml.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apache2/msc_xml.c b/apache2/msc_xml.c index 9222b0176d..abc26ba298 100644 --- a/apache2/msc_xml.c +++ b/apache2/msc_xml.c @@ -304,10 +304,12 @@ int xml_complete(modsec_rec *msr, char **error_msg) { /* Only if we have a context, meaning we've done some work. */ if (msr->xml->parsing_ctx != NULL || msr->xml->parsing_ctx_arg != NULL) { + int terminate = !msr->reqbody_partial_processing_enabled; + if (msr->xml->parsing_ctx != NULL && msr->txcfg->parse_xml_into_args != MSC_XML_ARGS_ONLYARGS) { /* This is how we signal the end of parsing to libxml. */ - xmlParseChunk(msr->xml->parsing_ctx, NULL, 0, 1); + xmlParseChunk(msr->xml->parsing_ctx, NULL, 0, terminate); /* Preserve the results for our reference. */ msr->xml->well_formed = msr->xml->parsing_ctx->wellFormed; @@ -326,7 +328,7 @@ int xml_complete(modsec_rec *msr, char **error_msg) { if (msr->xml->parsing_ctx_arg != NULL && msr->txcfg->parse_xml_into_args != MSC_XML_ARGS_OFF) { - if (xmlParseChunk(msr->xml->parsing_ctx_arg, NULL, 0, 1) != 0) { + if (xmlParseChunk(msr->xml->parsing_ctx_arg, NULL, 0, terminate) != 0) { if (msr->xml->xml_error) { *error_msg = msr->xml->xml_error; } From 371e687633488dc65af6a5727b66f1c243e1fd6e Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 26 Mar 2026 09:30:51 +0900 Subject: [PATCH 30/41] Implement request body partial processing for SecRequestBodyNoFilesLimit --- apache2/apache2_io.c | 14 ++++++++++++ apache2/msc_multipart.c | 24 +++++++++++++++++++++ apache2/msc_reqbody.c | 48 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c index a350e4d1bf..0220791f4f 100644 --- a/apache2/apache2_io.c +++ b/apache2/apache2_io.c @@ -301,8 +301,22 @@ apr_status_t read_request_body(modsec_rec *msr, char **error_msg) { 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; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Input filter: Bucket type %s shortened by %" APR_SIZE_T_FMT " bytes because of reqbody_limit and ProcessPartial.", + bucket->type->name, (apr_size_t)msr->txcfg->reqbody_limit - msr->reqbody_length); + } + finished_reading = 1; modsecurity_request_body_enable_partial_processing(msr); + } else if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Input filter: Bucket type %s skip storing because of no_files_limit and ProcessPartial.", + bucket->type->name); + } + buflen = 0; + finished_reading = 1; } msr->reqbody_length += buflen; diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 184c5d2c30..8c61b22a0c 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -273,6 +273,18 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { /* The buffer is data so increase the data length counter. */ msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + /* Enable partial processing if the no_files_length exceeds the limit and the limit action is ProcessPartial. */ + if ((msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + msr->mpd->bufleft -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "MULTIPART: length shortend by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } + if (len > 1) { if (msr->mpd->buf[len - 2] == '\r') { msr->mpd->flag_crlf_line = 1; @@ -587,6 +599,18 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { /* The buffer contains data so increase the data length counter. */ msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; + /* Enable partial processing if the no_files_length exceeds the limit and the limit action is ProcessPartial. */ + if ((msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + msr->mpd->bufleft -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "MULTIPART: length shortend by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } + /* add this part to the list of parts */ /* remember where we started */ diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index f660c24e40..21c3a4838b 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -362,6 +362,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "XML: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } + /* Process data as XML. */ if (xml_process_chunk(msr, data, length, &my_error_msg) < 0) { *error_msg = apr_psprintf(msr->mp, "XML parsing error: %s", my_error_msg); @@ -374,6 +386,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "JSON: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } + /* Process data as JSON. */ #ifdef WITH_YAJL if (json_process_chunk(msr, data, length, &my_error_msg) < 0) { @@ -393,6 +417,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "URLENCODED: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } + /* Do nothing else, URLENCODED processor does not support streaming. */ } else { @@ -403,6 +439,18 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } else if (msr->txcfg->reqbody_buffering != REQUEST_BODY_FORCEBUF_OFF) { /* Increase per-request data length counter if forcing buffering. */ msr->msc_reqbody_no_files_length += length; + + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "forceBuf: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } } /* Check that we are not over the request body no files limit. */ From 278d6c036b4e583a35e21f499e2b586b90abd536 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Thu, 26 Mar 2026 09:32:26 +0900 Subject: [PATCH 31/41] Adjust tests for request body partial processing --- .../10-reqbody-limit-action-forcebodybuf.t | 4 +- .../config/10-reqbody-limit-action-json.t | 428 +++++++- ...0-reqbody-limit-action-multipart-chunked.t | 980 +++++++++++++++++- .../10-reqbody-limit-action-urlencoded.t | 415 +++++++- .../config/10-reqbody-limit-action-xml.t | 414 +++++++- .../regression/config/10-request-directives.t | 511 +-------- tests/regression/rule/15-json.t | 1 + 7 files changed, 2201 insertions(+), 552 deletions(-) diff --git a/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t index e25794210a..ff92266428 100644 --- a/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t +++ b/tests/regression/config/10-reqbody-limit-action-forcebodybuf.t @@ -148,7 +148,7 @@ SecRule REQUEST_URI "/test.txt" "id:500219,phase:1,t:none,pass,ctl:forceRequestBodyVariable=On" ), match_log => { - error => [ qr/ModSecurity: Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + error => [ qr/ModSecurity: Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], debug => [ qr/enable_partial_processing for none reqbody_processor/, 1 ], }, match_response => { @@ -157,7 +157,7 @@ request => new HTTP::Request( POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ - "Content-Type" => "text/plain", + "Content-Type" => "text/plain", "Content-Length" => "16385", ], "a" x 16385, diff --git a/tests/regression/config/10-reqbody-limit-action-json.t b/tests/regression/config/10-reqbody-limit-action-json.t index 55dbdb0eca..92404a23b9 100644 --- a/tests/regression/config/10-reqbody-limit-action-json.t +++ b/tests/regression/config/10-reqbody-limit-action-json.t @@ -1,6 +1,6 @@ { type => "config", - comment => "SecRequestBodyLimitAction Reject (JSON, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction Reject (JSON, <=NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -10,6 +10,8 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -23,12 +25,12 @@ "Content-Type" => "application/json", "Content-Length" => "16384", ], - '{"a":"' . "1" x 16376 . '"}', + '{"a":"' . '1' x 16376 . '"}', ), }, { type => "config", - comment => "SecRequestBodyLimitAction Reject (JSON, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction Reject (JSON, <=NoFilesLimit, deny bad value)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -38,6 +40,68 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . '1' x 16360 . '","b":"bad_value"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, <=NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . '1' x 16363 . '","bad_name":1}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (JSON, >NoFilesLimit, too long)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -51,7 +115,7 @@ "Content-Type" => "application/json", "Content-Length" => "16385", ], - '{"a":"' . "1" x 16377 . '"}', + '{"a":"' . '1' x 16377 . '"}', ), }, { @@ -66,6 +130,8 @@ SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 1 ], @@ -79,7 +145,7 @@ "Content-Type" => "application/json", "Content-Length" => "16385", ], - '{"a":"' . "1" x 16377 . '"}', + '{"a":"' . '1' x 16377 . '"}', ), }, { @@ -94,6 +160,8 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(32768\)\./, 1 ], @@ -107,12 +175,162 @@ "Content-Type" => "application/json", "Content-Length" => "32769", ], - '{"a":"' . "1" x 32761 . '"}', + '{"a":"' . '1' x 32761 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, <=NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . '1' x 16376 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, <=NoFilesLimit, deny bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . '1' x 16360 . '","b":"bad_value"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, <=NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + '{"a":"' . '1' x 16363 . '","bad_name":1}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit, deny bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16361 . '","b":"bad_value",', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit, pass bad value)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -122,6 +340,8 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -135,12 +355,192 @@ "Content-Type" => "application/json", "Content-Length" => "16385", ], - '{"a":"' . "1" x 16377 . '"}', + '{"a":"' . '1' x 16361 . '","b":"bad_value "', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16364 . '","bad_name":1 ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >NoFilesLimit, pass bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16364 . '","bad_name": 1', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16377 . '"}', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit, deny bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16361 . '","b":"bad_value" ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit, pass bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16361 . '","b":" bad_value"', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + '{"a":"' . '1' x 16364 . '","bad_name":1 ', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, <=NoFilesLimit, pass bad name)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -150,6 +550,8 @@ SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 1 ], @@ -163,12 +565,12 @@ "Content-Type" => "application/json", "Content-Length" => "16385", ], - '{"a":"' . "1" x 16377 . '"}', + '{"a":"' . '1' x 16364 . '","bad_name": 1', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (JSON, >Limit, >NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -178,6 +580,8 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "application/json" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(32768\)\./, 1 ], @@ -191,6 +595,6 @@ "Content-Type" => "application/json", "Content-Length" => "32769", ], - '{"a":"' . "1" x 32761 . '"}', + '{"a":"' . '1' x 32761 . '"}', ), }, diff --git a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t index b84bbb2d4c..f05690eeda 100644 --- a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t +++ b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t @@ -1,6 +1,6 @@ { type => "config", - comment => "SecRequestBodyLimitAction Reject (multipart, chunked, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, <=NoFilesLimit, no bad)", conf => trim_action_indent( qq( SecRuleEngine On @@ -11,7 +11,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ - "id:'200003',phase:2,t:none,log, \\ + "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ @@ -26,10 +26,13 @@ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}, \\ UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ) ), match_log => { error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { status => qr/^200$/, @@ -61,6 +64,138 @@ 4096 ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, <=NoFilesLimit, deny bad name)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Multipart flags: \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + debug => [ qr/Request body no files length: 16384/, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16271 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (multipart, chunked, <=NoFilesLimit, deny bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Multipart flags: \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + debug => [ qr/Request body no files length: 16384/, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . 'b' x 16269 . q(bad_value + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, { type => "config", comment => "SecRequestBodyLimitAction Reject (multipart, chunked, >NoFilesLimit)", @@ -74,7 +209,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ - "id:'200003',phase:2,t:none,log, \\ + "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ @@ -89,10 +224,13 @@ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}, \\ UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ) ), match_log => { -error => [ qr/Multipart flags:/, 1], + debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], }, match_response => { status => qr/^413$/, @@ -137,7 +275,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ - "id:'200003',phase:2,t:none,log, \\ + "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ @@ -152,10 +290,13 @@ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}, \\ UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ) ), match_log => { -error => [ qr/Multipart flags:/, 1], + debug => [ qr/Request body is larger than the configured limit \(32768\)\./, 1], }, match_response => { status => qr/^413$/, @@ -189,7 +330,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =NoFilesLimit, no bad)", conf => trim_action_indent( qq( SecRuleEngine On @@ -200,7 +341,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ - "id:'200003',phase:2,t:none,log, \\ + "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ PE %{REQBODY_PROCESSOR_ERROR}, \\ @@ -216,11 +357,14 @@ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}, \\ UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), 4096 ), match_log => { error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { status => qr/^200$/, @@ -245,7 +389,7 @@ -----------------------------69343412719991675451336310646 Content-Disposition: form-data; name="b" - ) . "b" x 16279 . q( + ) . "b" x 16278 . q( -----------------------------69343412719991675451336310646-- ) ), @@ -254,7 +398,7 @@ }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >Limit)", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =NoFilesLimit, deny bad name)", conf => trim_action_indent( qq( SecRuleEngine On @@ -265,8 +409,8 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ - "id:'200003',phase:2,t:none,log, \\ - msg:'Check values for test: \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ @@ -281,10 +425,150 @@ IH %{MULTIPART_INVALID_HEADER_FOLDING}, \\ FL %{MULTIPART_FILE_LIMIT_EXCEEDED}, \\ UB %{MULTIPART_UNMATCHED_BOUNDARY}'" - ) + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + 4096 ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files length: 16384/, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16271 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =NoFilesLimit, deny bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + 4096 + ), + match_log => { + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files length: 16384/, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . 'b' x 16269 . q(bad_value + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >NoFilesLimit, no bad)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + 4096 + ), + match_log => { + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], }, match_response => { status => qr/^200$/, @@ -305,14 +589,682 @@ -----------------------------69343412719991675451336310646 Content-Disposition: form-data; name="a"; filename="a.txt" - ) . "a" x 16199 . q( + ) . "a" x 4096 . q( -----------------------------69343412719991675451336310646 Content-Disposition: form-data; name="b" - ) . "b" x 16278 . q( + ) . "b" x 16279 . q( -----------------------------69343412719991675451336310646-- ) ), 4096 ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >NoFilesLimit, deny bad name)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + 4096 + ), + match_log => { + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16272 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >NoFilesLimit, deny bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + 4096 + ), + match_log => { + error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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"; filename="a.txt" + + ) . "a" x 4096 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . 'b' x 16270 . q(bad_value + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =Limit, no bad)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16278 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =Limit, deny bad name)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16271 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, =Limit, deny bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16270 . q(ad_value + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart, chunked, >Limit, no bad)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Multipart: Allow partial processing of request body/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16279 . q( + -----------------------------69343412719991675451336310646-- + ) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart no CRLF after final boundary, chunked, >Limit, deny bad name)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Multipart: Allow partial processing of request body/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16274 . q( + -----------------------------69343412719991675451336310646--) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart no CRLF after final boundary, chunked, >Limit, pass bad name)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Multipart: Allow partial processing of request body/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="bad_name" + + ) . "b" x 16275 . q( + -----------------------------69343412719991675451336310646--) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart no CRLF after final boundary, chunked, >Limit, deny bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Multipart: Allow partial processing of request body/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16273 . q(ad_value + -----------------------------69343412719991675451336310646--) + ), + 4096 + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (multipart no CRLF after final boundary, chunked, >Limit, pass bad value)", + conf => trim_action_indent( + qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ + "id:'200000',phase:2,t:none,log, \\ + msg:'Check values for test: \\ + RE %{REQBODY_ERROR}, \\ + 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}, \\ + UB %{MULTIPART_UNMATCHED_BOUNDARY}'" + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ) + ), + match_log => { + error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Multipart: Allow partial processing of request body/, 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"; filename="a.txt" + + ) . "a" x 16199 . q( + -----------------------------69343412719991675451336310646 + Content-Disposition: form-data; name="b" + + ) . "b" x 16274 . q(ad_value + -----------------------------69343412719991675451336310646--) + ), + 4096 + ), +}, diff --git a/tests/regression/config/10-reqbody-limit-action-urlencoded.t b/tests/regression/config/10-reqbody-limit-action-urlencoded.t index 85f823ad99..7c2135b277 100644 --- a/tests/regression/config/10-reqbody-limit-action-urlencoded.t +++ b/tests/regression/config/10-reqbody-limit-action-urlencoded.t @@ -1,6 +1,6 @@ { type => "config", - comment => "SecRequestBodyLimitAction Reject (urlencoded, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction Reject (urlencoded, <=NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -9,6 +9,8 @@ SecRequestBodyLimitAction Reject SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -22,7 +24,65 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "16384", ], - "a=1&b=" . "2" x 16378, + 'a=1&b=' . '2' x 16378, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, <=NoFilesLimit, deny bad_value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + 'a=' . '1' x 16370 . '&b=bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (urlencoded, <=NoFilesLimit, deny bad_name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + 'a=' . '1' x 16373 . '&bad_name', ), }, { @@ -36,6 +96,8 @@ SecRequestBodyLimitAction Reject SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -49,7 +111,7 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "16385", ], - "a=1&b=" . "2" x 16379, + 'a=1&b=' . '2' x 16379, ), }, { @@ -63,6 +125,8 @@ SecRequestBodyLimitAction Reject SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 1 ], @@ -76,7 +140,7 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "16385", ], - "a=1&b=" . "2" x 16379, + 'a=1&b=' . '2' x 16379, ), }, { @@ -90,6 +154,8 @@ SecRequestBodyLimitAction Reject SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(32768\)\./, 1 ], @@ -103,12 +169,215 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "32769", ], - "a=1&b=" . "2" x 32763, + 'a=1&b=' . '2' x 32763, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, <=NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + 'a=1&b=' . '2' x 16378, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, <=NoFilesLimit, deny bad_value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + 'a=' . '1' x 16370 . '&b=bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, <=NoFilesLimit, deny bad_name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16384", + ], + 'a=' . '1' x 16373 . '&bad_name', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16379 . '&b=2', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit, deny bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16369 . '&b=bad_value&c', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit, pass bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16370 . '&b=bad_value&', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16370 . '&bad_name=2&c', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >NoFilesLimit, deny bad name)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -117,6 +386,8 @@ SecRequestBodyLimitAction ProcessPartial SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',phase:2,t:none,deny" + SecRule ARGS "bad_value" "id:'200003',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -130,12 +401,12 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "16385", ], - "a=1&b=" . "2" x 16379, + 'a=' . '1' x 16371 . '&bad_name=2&', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -144,6 +415,8 @@ SecRequestBodyLimitAction ProcessPartial SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 1 ], @@ -157,12 +430,128 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "16385", ], - "a=1&b=" . "2" x 16379, + 'a=1&b=' . '2' x 16379, ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit, deny bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16369 . '&b=bad_value&c', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit, pass bad value)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16370 . '&b=bad_value&', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit, deny bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16370 . '&bad_name=2&c', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, <=NoFilesLimit, pass bad name)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(16384\)\./, 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" => "16385", + ], + 'a=' . '1' x 16371 . '&bad_name=2&', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (urlencoded, >Limit, >NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -171,6 +560,8 @@ SecRequestBodyLimitAction ProcessPartial SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 + SecRule ARGS_NAMES "bad_name" "id:'200002',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 \(32768\)\./, 1 ], @@ -184,6 +575,6 @@ "Content-Type" => "application/x-www-form-urlencoded", "Content-Length" => "32769", ], - "a=1&b=" . "2" x 32763, + 'a=1&b=' . '2' x 32763, ), -}, \ No newline at end of file +}, diff --git a/tests/regression/config/10-reqbody-limit-action-xml.t b/tests/regression/config/10-reqbody-limit-action-xml.t index 4cccd6b143..5e08dd045d 100644 --- a/tests/regression/config/10-reqbody-limit-action-xml.t +++ b/tests/regression/config/10-reqbody-limit-action-xml.t @@ -1,6 +1,6 @@ { type => "config", - comment => "SecRequestBodyLimitAction Reject (XML, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction Reject (XML, <=NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -10,6 +10,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -23,7 +24,36 @@ "Content-Type" => "application/xml", "Content-Length" => "16384", ], - '' . "1" x 16364 . '', + '' . '1' x 16364 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction Reject (XML, <=NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction Reject + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16355 . 'bad_value', ), }, { @@ -38,6 +68,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -51,7 +82,7 @@ "Content-Type" => "application/xml", "Content-Length" => "16385", ], - '' . "1" x 16365 . '', + '' . '1' x 16365 . '', ), }, { @@ -66,6 +97,7 @@ SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], @@ -79,7 +111,7 @@ "Content-Type" => "application/xml", "Content-Length" => "16385", ], - '' . "1" x 16365 . '', + '' . '1' x 16365 . '', ), }, { @@ -94,6 +126,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], @@ -107,12 +140,99 @@ "Content-Type" => "application/xml", "Content-Length" => "32769", ], - '' . "1" x 32749 . '', + '' . '1' x 32749 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, <=NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16364 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, <=NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16355 . 'bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, no bad)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16365 . '', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, deny)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -122,6 +242,72 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16366 . 'bad_value ', + ), +}, + +# The four tests below checks whether the partial text content is added or not. +# +# xmlParseChunk adds a text content whose length is greater than or equal to 300 bytes even when +# you pass 0 as the terminate argument. If the length is less than 300 bytes it does not a text content +# at end of the chunk without a tag being followed. +# https://gitlab.gnome.org/GNOME/libxml2/-/blob/ca6a8cf94672e6f3c48c08d6af2201599788fc17/parser.c#L11061-11079 +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, chunk_len>=300, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16068 . '' . '2' x 291 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, chunk_len<300, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], @@ -135,12 +321,100 @@ "Content-Type" => "application/xml", "Content-Length" => "16385", ], - '' . "1" x 16365 . '', + '' . '1' x 16069 . '' . '2' x 290 . 'bad_value ', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, <=NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, nested, chunk_len>=300, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16072 . '' . '2' x 291 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, nested, chunk_len<300, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16073 . '' . '2' x 290 . 'bad_value ', + ), +}, + +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16366 . ' bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, <=NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -150,6 +424,7 @@ SecRequestBodyNoFilesLimit 32768 SecRequestBodyLimit 16384 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], @@ -163,12 +438,70 @@ "Content-Type" => "application/xml", "Content-Length" => "16385", ], - '' . "1" x 16365 . '', + '' . '1' x 16365 . '', ), }, { type => "config", - comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, >NoFilesLimit)", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, <=NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16366 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, <=NoFilesLimit, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 32768 + SecRequestBodyLimit 16384 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16385", + ], + '' . '1' x 16366 . ' bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, >NoFilesLimit, no bad)", conf => qq( SecRuleEngine On SecDebugLog $ENV{DEBUG_LOG} @@ -178,6 +511,7 @@ SecRequestBodyNoFilesLimit 16384 SecRequestBodyLimit 32768 SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" ), match_log => { error => [ qr/Request body \(Content-Length\) is larger than the configured limit \(32768\)\./, 1 ], @@ -191,6 +525,64 @@ "Content-Type" => "application/xml", "Content-Length" => "32769", ], - '' . "1" x 32749 . '', + '' . '1' x 32749 . '', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, >NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "32769", + ], + '' . '1' x 16366 . 'bad_value ' . '1' x 16384, + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, >Limit, >NoFilesLimit, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "32769", + ], + '' . '1' x 16366 . ' bad_value' . '1' x 16384, ), }, diff --git a/tests/regression/config/10-request-directives.t b/tests/regression/config/10-request-directives.t index a2eeaba23d..8ca6035da7 100644 --- a/tests/regression/config/10-request-directives.t +++ b/tests/regression/config/10-request-directives.t @@ -1857,14 +1857,11 @@ SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyNoFilesLimit 15 + SecRequestBodyNoFilesLimit 16 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$/, }, @@ -1872,6 +1869,7 @@ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ "Content-Type" => "application/x-www-form-urlencoded", + "Content-Length" => "16", ], normalize_raw_request_data( q(a=1&b=2&bad_name), @@ -2256,7 +2254,8 @@ SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 11 + SecRequestBodyNoFilesLimit 13 + SecRequestBodyLimit 13 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 @@ -2268,10 +2267,9 @@ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ "Content-Type" => "application/xml", + "Content-Length" => "14", ], - normalize_raw_request_data( - q(bad_value), - ), + 'bad_value <', ), }, { @@ -2283,7 +2281,8 @@ SecDebugLogLevel 9 SecRequestBodyAccess On SecRequestBodyLimitAction ProcessPartial - SecRequestBodyLimit 12 + SecRequestBodyNoFilesLimit 13 + SecRequestBodyLimit 13 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 @@ -2295,10 +2294,9 @@ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ "Content-Type" => "application/xml", + "Content-Length" => "14", ], - normalize_raw_request_data( - q(bad_value), - ), + 'bad_value< ', ), }, { @@ -2498,492 +2496,3 @@ "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" => "8219", - ], - '{"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 - ), -}, diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index 5c19a382ba..a62ea7fd92 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -320,6 +320,7 @@ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", [ "Content-Type" => "application/json", + "Content-Length" => "27", ], q({"a":12345,"b":"bad_value"}), ), From f87485554e2cbfee355186ef99df5d4dfc14e9b2 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 27 Mar 2026 14:56:28 +0900 Subject: [PATCH 32/41] Add bad format tests --- tests/regression/rule/10-xml.t | 136 ++++++++++++++++++++++++++++++++ tests/regression/rule/15-json.t | 67 ++++++++++++++++ 2 files changed, 203 insertions(+) diff --git a/tests/regression/rule/10-xml.t b/tests/regression/rule/10-xml.t index 673651226a..17ced4429d 100644 --- a/tests/regression/rule/10-xml.t +++ b/tests/regression/rule/10-xml.t @@ -428,6 +428,142 @@ ), ), }, +{ + type => "rule", + comment => "xml ProcessPartial, bad format and whole body before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 57 + SecRequestBodyNoFilesLimit 57 + 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 REQBODY_ERROR "!\@eq 0" \\ + "id:'500007', 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:'500008',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 400 \(phase 2\). Match of "eq 0" against "REQBODY_ERROR" required\./, 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" => "57", + ], + 'value "rule", + comment => "xml ProcessPartial, bad format and length exceeds limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 57 + SecRequestBodyNoFilesLimit 57 + 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 REQBODY_ERROR "!\@eq 0" \\ + "id:'500007', 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:'500008',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Access denied with code 400 \(phase 2\). Match of "eq 0" against "REQBODY_ERROR" required\./, 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" => "58", + ], + 'value "rule", + comment => "xml ProcessPartial, bad format and whole body before limit, no declaration", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 19 + SecRequestBodyNoFilesLimit 19 + 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 REQBODY_ERROR "!\@eq 0" \\ + "id:'500007', 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:'500008',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Access denied with code 400 \(phase 2\). Match of "eq 0" against "REQBODY_ERROR" required\./, 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" => "19", + ], + 'value "rule", + comment => "xml ProcessPartial, bad format and length exceeds limit, no declaration", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyLimit 19 + SecRequestBodyNoFilesLimit 19 + 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 REQBODY_ERROR "!\@eq 0" \\ + "id:'500007', 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:'500008',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Access denied with code 400 \(phase 2\). Match of "eq 0" against "REQBODY_ERROR" required\./, 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" => "20", + ], + 'value "rule", comment => "xml ProcessPartial, bad value and whole body before limit", diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t index a62ea7fd92..a85ecaf78f 100644 --- a/tests/regression/rule/15-json.t +++ b/tests/regression/rule/15-json.t @@ -259,6 +259,73 @@ ), ), }, +{ + type => "rule", + comment => "LimitAction ProcessPartial, bad format and whole body before limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 21 + SecRequestBodyLimit 21 + 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 400 \(phase 2\)\. Match of "eq 0" against "REQBODY_ERROR" required\./, 1 ], + debug => [ qr/Adding JSON argument 'b' with value 'value'|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/^400$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/json", + "Content-Length" => "21", + ], + q({"a":1234,"b":"value"), + ), +}, +{ + type => "rule", + comment => "LimitAction ProcessPartial, bad format and length exceeds limit", + conf => qq( + SecRuleEngine On + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 21 + SecRequestBodyLimit 21 + 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 400 \(phase 2\)\. Match of "eq 0" against "REQBODY_ERROR" required\./, 1 ], + debug => [ qr/Adding JSON argument 'b' with value 'value'|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", + "Content-Length" => "22", + ], + q({"a":1234,"b":"value" ), + ), +}, { type => "rule", comment => "LimitAction ProcessPartial, bad value and whole body before limit", From ad9418687e6c7ead9d926f6fd688346cc0e257ad Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 27 Mar 2026 15:46:12 +0900 Subject: [PATCH 33/41] Add short xml tests for ProcessPartial --- .../config/10-reqbody-limit-action-xml.t | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/tests/regression/config/10-reqbody-limit-action-xml.t b/tests/regression/config/10-reqbody-limit-action-xml.t index 5e08dd045d..d82f1cdac4 100644 --- a/tests/regression/config/10-reqbody-limit-action-xml.t +++ b/tests/regression/config/10-reqbody-limit-action-xml.t @@ -260,7 +260,7 @@ ), }, -# The four tests below checks whether the partial text content is added or not. +# The 8 tests below checks whether the partial text content is added or not. # # xmlParseChunk adds a text content whose length is greater than or equal to 300 bytes even when # you pass 0 as the terminate argument. If the length is less than 300 bytes it does not a text content @@ -382,6 +382,122 @@ '' . '1' x 16073 . '' . '2' x 290 . 'bad_value ', ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, short, >NoFilesLimit, chunk_len>=300, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 1024 + SecRequestBodyLimit 2048 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(1024\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "1025", + ], + '' . '1' x 708 . '' . '2' x 291 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, short, >NoFilesLimit, chunk_len<300, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 1024 + SecRequestBodyLimit 2048 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(1024\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "1025", + ], + '' . '1' x 709 . '' . '2' x 290 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, short, >NoFilesLimit, nested, chunk_len>=300, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 1024 + SecRequestBodyLimit 2048 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(1024\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "1025", + ], + '' . '1' x 712 . '' . '2' x 291 . 'bad_value ', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, short, >NoFilesLimit, nested, chunk_len<300, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 1024 + SecRequestBodyLimit 2048 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + error => [ qr/Request body no files data length is larger than the configured limit \(1024\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "1025", + ], + '' . '1' x 713 . '' . '2' x 290 . 'bad_value ', + ), +}, { type => "config", From 1634a2a707f9c0a93098b9a4a7b53ca8351c31de Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 27 Mar 2026 15:53:15 +0900 Subject: [PATCH 34/41] Add more XML tests, leaf or no-leaf, ARGS --- .../config/10-reqbody-limit-action-xml.t | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/regression/config/10-reqbody-limit-action-xml.t b/tests/regression/config/10-reqbody-limit-action-xml.t index d82f1cdac4..faa58b13f6 100644 --- a/tests/regression/config/10-reqbody-limit-action-xml.t +++ b/tests/regression/config/10-reqbody-limit-action-xml.t @@ -201,6 +201,95 @@ '' . '1' x 16355 . 'bad_value', ), }, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, non-leaf, <=NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule XML:/* "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16351 . 'bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, ARGS, non-leaf, <=NoFilesLimit, pass)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecParseXmlIntoArgs On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^200$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16351 . 'bad_value', + ), +}, +{ + type => "config", + comment => "SecRequestBodyLimitAction ProcessPartial (XML, ARGS, leaf, <=NoFilesLimit, deny)", + conf => qq( + SecRuleEngine On + SecDebugLog $ENV{DEBUG_LOG} + SecDebugLogLevel 9 + SecRequestBodyAccess On + SecParseXmlIntoArgs On + SecRequestBodyLimitAction ProcessPartial + SecRequestBodyNoFilesLimit 16384 + SecRequestBodyLimit 32768 + SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\\+|/)|text/)xml" "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + SecRule ARGS "bad_value" "id:'200002',phase:2,t:none,deny" + ), + match_log => { + -error => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1 ], + }, + match_response => { + status => qr/^403$/, + }, + request => new HTTP::Request( + POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", + [ + "Content-Type" => "application/xml", + "Content-Length" => "16384", + ], + '' . '1' x 16355 . 'bad_value', + ), +}, { type => "config", comment => "SecRequestBodyLimitAction ProcessPartial (XML, >NoFilesLimit, no bad)", From 7c4fee5d4e02ea04b1f38243d179c05069fb157a Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 27 Mar 2026 17:08:54 +0900 Subject: [PATCH 35/41] Reduce code duplication by adding modsecurity_request_body_enable_partial_processing_for_no_files_length --- apache2/msc_multipart.c | 43 +++++++++++-------------- apache2/msc_reqbody.c | 70 +++++++++++++---------------------------- 2 files changed, 41 insertions(+), 72 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 8c61b22a0c..8c4d2842d7 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -251,6 +251,23 @@ static int multipart_parse_content_disposition(modsec_rec *msr, char *c_d_value) return 1; } +/* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ +static void modsecurity_request_body_enable_partial_processing_for_no_files_length(modsec_rec *msr, + int *length) +{ + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + *length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "MULTIPART: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } +} + /** * */ @@ -272,18 +289,7 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { /* The buffer is data so increase the data length counter. */ msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); - - /* Enable partial processing if the no_files_length exceeds the limit and the limit action is ProcessPartial. */ - if ((msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - msr->mpd->bufleft -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "MULTIPART: length shortend by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); if (len > 1) { if (msr->mpd->buf[len - 2] == '\r') { @@ -598,18 +604,7 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { /* The buffer contains data so increase the data length counter. */ msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; - - /* Enable partial processing if the no_files_length exceeds the limit and the limit action is ProcessPartial. */ - if ((msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - msr->mpd->bufleft -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "MULTIPART: length shortend by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); /* add this part to the list of parts */ diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 21c3a4838b..f8ceed8043 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -312,6 +312,24 @@ static apr_status_t modsecurity_request_body_store_memory(modsec_rec *msr, return 1; } +/* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ +static void modsecurity_request_body_enable_partial_processing_for_no_files_length(modsec_rec *msr, + apr_size_t *length, const char *reqbody_processor) +{ + /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ + if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) + && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) + { + *length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "%s: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + reqbody_processor, + msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + } + modsecurity_request_body_enable_partial_processing(msr); + } +} + /** * Stores one chunk of request body data. Returns -1 on error. */ @@ -361,18 +379,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, else if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; - - /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "XML: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "XML"); /* Process data as XML. */ if (xml_process_chunk(msr, data, length, &my_error_msg) < 0) { @@ -385,18 +392,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; - - /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "JSON: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "JSON"); /* Process data as JSON. */ #ifdef WITH_YAJL @@ -416,18 +412,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { /* Increase per-request data length counter. */ msr->msc_reqbody_no_files_length += length; - - /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "URLENCODED: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "URLENCODED"); /* Do nothing else, URLENCODED processor does not support streaming. */ } @@ -439,18 +424,7 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } else if (msr->txcfg->reqbody_buffering != REQUEST_BODY_FORCEBUF_OFF) { /* Increase per-request data length counter if forcing buffering. */ msr->msc_reqbody_no_files_length += length; - - /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "forceBuf: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } + modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "forceBuf"); } /* Check that we are not over the request body no files limit. */ From b57f252436822e70fe5bb55fabd7033c4ee062b1 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 7 Apr 2026 11:57:27 +0900 Subject: [PATCH 36/41] Use strnlen for multipart boundary for safety To make SonarQube happy --- apache2/msc_multipart.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 8c4d2842d7..19e086bff4 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -20,6 +20,8 @@ #include "msc_util.h" #include "msc_parsers.h" +#define CONTENT_TYPE_MAX_LENGTH 1024 + void validate_quotes(modsec_rec *msr, char *data, char quote) { assert(msr != NULL); int i, len; @@ -841,7 +843,7 @@ int multipart_init(modsec_rec *msr, char **error_msg) { return -1; } - if (strlen(msr->request_content_type) > 1024) { + if (strlen(msr->request_content_type) > CONTENT_TYPE_MAX_LENGTH) { msr->mpd->flag_error = 1; *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid boundary in C-T (length)."); return -1; @@ -1063,7 +1065,7 @@ 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 boundary_len = strlen(msr->mpd->boundary); + size_t boundary_len = strnlen(msr->mpd->boundary, CONTENT_TYPE_MAX_LENGTH); if ( (buf_data_len >= 2 + boundary_len) && (*(msr->mpd->buf) == '-') && (*(msr->mpd->buf + 1) == '-') From cf35aa24dba8fa501c4eb838d5b5e0d981c82233 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Tue, 30 Jun 2026 16:55:19 +0900 Subject: [PATCH 37/41] Debug: log part header and data bytes in multipart parser --- apache2/Makefile.am | 1 + apache2/dbg_print_bytes.c | 153 ++++++++++++++++++++++++++++++++++++++ apache2/dbg_print_bytes.h | 28 +++++++ apache2/msc_multipart.c | 15 ++++ 4 files changed, 197 insertions(+) create mode 100644 apache2/dbg_print_bytes.c create mode 100644 apache2/dbg_print_bytes.h diff --git a/apache2/Makefile.am b/apache2/Makefile.am index 11b6c69e1a..f6971abfdd 100644 --- a/apache2/Makefile.am +++ b/apache2/Makefile.am @@ -5,6 +5,7 @@ mod_security2_la_SOURCES = acmp.c \ apache2_config.c \ apache2_io.c \ apache2_util.c \ + dbg_print_bytes.c \ others/libinjection/src/libinjection_html5.c \ others/libinjection/src/libinjection_sqli.c \ others/libinjection/src/libinjection_xss.c \ diff --git a/apache2/dbg_print_bytes.c b/apache2/dbg_print_bytes.c new file mode 100644 index 0000000000..7d71fd87ec --- /dev/null +++ b/apache2/dbg_print_bytes.c @@ -0,0 +1,153 @@ +#include "dbg_print_bytes.h" +#include +#include + +/* ---- internal helpers ---- */ + +/** + * Return non-zero if byte c needs escaping in the output. + * Backslash (0x5C) is escaped as \\ to avoid ambiguity + * with escape sequences. Other non-printable/control/high bytes + * are escaped as \xNN. + */ +static int needs_escape(unsigned char c) { + return !(0x20 <= c && c <= 0x7E) || c == '\\'; +} + +/** + * Escaped output length for byte c: + * 1 when kept as-is (safe printable) + * 2 for backslash (escaped as \\) + * 4 otherwise (escaped as \xNN) + */ +static size_t esc_len(unsigned char c) { + if (c == '\\') return 2; + return needs_escape(c) ? 4 : 1; +} + +/** + * Write byte c into dest[pos..] with proper escaping. + * Returns the number of bytes written. + */ +static size_t write_byte(char *dest, size_t pos, unsigned char c) { + if (!needs_escape(c)) { + dest[pos] = (char)c; + return 1; + } + if (c == '\\') { + dest[pos + 0] = '\\'; + dest[pos + 1] = '\\'; + return 2; + } + /* \xNN (lowercase) */ + static const char hex[] = "0123456789abcdef"; + dest[pos + 0] = '\\'; + dest[pos + 1] = 'x'; + dest[pos + 2] = hex[c >> 4]; + dest[pos + 3] = hex[c & 0x0f]; + return 4; +} + +/** + * Escape and write src[0..len) into dest starting at pos. + * Returns the new position after writing. + */ +static size_t write_bytes(char *dest, size_t pos, + const unsigned char *src, size_t len) { + for (size_t i = 0; i < len; i++) { + pos += write_byte(dest, pos, src[i]); + } + return pos; +} + +/* ---- public API ---- */ + +size_t dbg_print_bytes(char *dest, size_t dest_size, + const unsigned char *src, size_t src_len, + const char *omit) +{ + /* error / empty input */ + if (dest == NULL || dest_size == 0) { + return 0; + } + if (src == NULL || src_len == 0) { + dest[0] = '\0'; + return 0; + } + + /* compute total escaped length */ + size_t total = 0; + for (size_t i = 0; i < src_len; i++) { + total += esc_len(src[i]); + } + + const size_t max_out = dest_size - 1; /* reserve room for null terminator */ + + /* ---- fits without truncation ---- */ + if (total <= max_out) { + size_t pos = write_bytes(dest, 0, src, src_len); + dest[pos] = '\0'; + return total; + } + + /* ---- truncation needed ---- */ + const char *mid = omit ? omit : "..."; + size_t mid_len = strlen(mid); + size_t head_len, tail_len; + int use_mid = 1; /* 1: include marker, 0: omit marker */ + + if (max_out < mid_len + 2) { + /* not enough room for marker + at least 2 chars */ + use_mid = 0; + head_len = max_out; + tail_len = 0; + } else { + head_len = (max_out - mid_len + 1) / 2; /* round up */ + tail_len = (max_out - mid_len) - head_len; /* remainder */ + } + + /* --- determine head range (up to head_len escaped chars from start) --- */ + size_t head_end = 0; /* index into src (exclusive) */ + { + size_t acc = 0; + for (size_t i = 0; i < src_len; i++) { + size_t len = esc_len(src[i]); + if (acc + len > head_len) break; + acc += len; + head_end = i + 1; + } + } + + /* --- determine tail range (up to tail_len escaped chars from end) --- */ + size_t tail_start = src_len; /* index into src (start of tail portion) */ + if (tail_len > 0) { + size_t acc = 0; + for (size_t i = src_len; i > 0; i--) { + size_t len = esc_len(src[i - 1]); + if (acc + len > tail_len) break; + acc += len; + tail_start = i - 1; + } + } + + /* --- write to output buffer --- */ + size_t pos = 0; + + /* head portion */ + pos = write_bytes(dest, pos, src, head_end); + + /* middle omission marker (only if there is a gap between head and tail) */ + if (use_mid && head_end < tail_start) { + memcpy(dest + pos, mid, mid_len); + pos += mid_len; + } + + /* tail portion */ + if (tail_start < src_len) { + pos = write_bytes(dest, pos, src + tail_start, + src_len - tail_start); + } + + dest[pos] = '\0'; + return total; +} diff --git a/apache2/dbg_print_bytes.h b/apache2/dbg_print_bytes.h new file mode 100644 index 0000000000..c5fc030c9c --- /dev/null +++ b/apache2/dbg_print_bytes.h @@ -0,0 +1,28 @@ +#ifndef DBG_PRINT_BYTES_H +#define DBG_PRINT_BYTES_H + +#include + +/** + * @brief Format an arbitrary byte sequence for debug output into a fixed-size buffer + * + * Bytes in the safe printable range (0x20-0x7E except backslash 0x5C) are output + * as-is. Backslash is escaped to \\ to avoid ambiguity. All other bytes are + * escaped in \xNN format (lowercase hexadecimal, zero-padded). + * If the escaped output does not fit in the buffer, the middle part is omitted + * and replaced with the omission marker, keeping the head and tail portions. + * + * @param dest Output buffer. If NULL, returns 0 without doing anything. + * @param dest_size Size of the output buffer in bytes. If 0, returns 0. + * @param src Input byte sequence. If NULL or src_len is 0, treated as empty. + * @param src_len Length of the input byte sequence in bytes. + * @param omit Omission marker string inserted between head and tail when + * truncation occurs. If NULL, defaults to "...". + * @return The total length of the fully escaped string (excluding null terminator). + * If the return value is >= dest_size, truncation (omission) occurred. + */ +size_t dbg_print_bytes(char *dest, size_t dest_size, + const unsigned char *src, size_t src_len, + const char *omit); + +#endif /* DBG_PRINT_BYTES_H */ diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index 19e086bff4..40c73587f7 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -19,9 +19,13 @@ #include "msc_multipart.h" #include "msc_util.h" #include "msc_parsers.h" +#include "dbg_print_bytes.h" #define CONTENT_TYPE_MAX_LENGTH 1024 +#define DEBUG_BYTES_BUF_LEN 256 +#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." + void validate_quotes(modsec_rec *msr, char *data, char quote) { assert(msr != NULL); int i, len; @@ -277,6 +281,7 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { assert(msr != NULL); assert(error_msg != NULL); int i, len, rc; + char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -290,6 +295,10 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { } /* The buffer is data so increase the data length counter. */ + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, + MULTIPART_BUF_SIZE - msr->mpd->bufleft, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] Multipart: adding part_header, no_files_len=%lu, add_len=%d, buf=%s", + msr->msc_reqbody_no_files_length, (MULTIPART_BUF_SIZE - msr->mpd->bufleft), debug_buf); msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); @@ -490,6 +499,7 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { char *p = msr->mpd->buf + (MULTIPART_BUF_SIZE - msr->mpd->bufleft); char localreserve[2] = { '\0', '\0' }; /* initialized to quiet warning */ int bytes_reserved = 0; + char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -605,6 +615,11 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { value_part_t *value_part = apr_pcalloc(msr->mp, sizeof(value_part_t)); /* The buffer contains data so increase the data length counter. */ + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, + (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] Multipart: adding part_form_data, no_files_len=%lu, add_len=%d, buf=%s", + msr->msc_reqbody_no_files_length, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], debug_buf); + int len = (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); From 3c7a55c67762f3eda38de261466acdaccf4790c6 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Wed, 1 Jul 2026 16:31:12 +0900 Subject: [PATCH 38/41] Make tests pass --- apache2/apache2_io.c | 25 +- apache2/modsecurity.h | 4 +- apache2/msc_multipart.c | 44 ++-- apache2/msc_parsers.c | 9 + apache2/msc_reqbody.c | 103 ++++---- apache2/msc_reqbody.h | 24 ++ iis/ModSecurityIIS.vcxproj | 1 + iis/ModSecurityIIS.vcxproj.filters | 3 + standalone/Makefile.am | 329 +++++++++++++------------- standalone/modules.mk | 2 +- standalone/standalone.vcxproj.filters | 3 + 11 files changed, 295 insertions(+), 252 deletions(-) create mode 100644 apache2/msc_reqbody.h diff --git a/apache2/apache2_io.c b/apache2/apache2_io.c index 0220791f4f..cf76c25086 100644 --- a/apache2/apache2_io.c +++ b/apache2/apache2_io.c @@ -17,6 +17,7 @@ #include "modsecurity.h" #include "apache2.h" #include "msc_crypt.h" +#include "msc_reqbody.h" #ifdef APLOG_USE_MODULE APLOG_USE_MODULE(security2); @@ -299,18 +300,20 @@ 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; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "Input filter: Bucket type %s shortened by %" APR_SIZE_T_FMT " bytes because of reqbody_limit and ProcessPartial.", - bucket->type->name, (apr_size_t)msr->txcfg->reqbody_limit - msr->reqbody_length); - } + if (msr->reqbody_length + buflen > (apr_size_t)msr->txcfg->reqbody_limit) { + msr->reqbody_length_limit_exceeded = 1; + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { + buflen = (apr_size_t)msr->txcfg->reqbody_limit - msr->reqbody_length; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "Input filter: Bucket type %s shortened by %" APR_SIZE_T_FMT " bytes because of reqbody_limit and ProcessPartial.", + bucket->type->name, (apr_size_t)msr->txcfg->reqbody_limit - msr->reqbody_length); + } - finished_reading = 1; - modsecurity_request_body_enable_partial_processing(msr); - } else if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { + finished_reading = 1; + modsecurity_request_body_do_enable_partial_processing(msr); + } + } + if (msr->reqbody_no_files_length_limit_exceeded) { if (msr->txcfg->debuglog_level >= 9) { msr_log(msr, 9, "Input filter: Bucket type %s skip storing because of no_files_limit and ProcessPartial.", bucket->type->name); diff --git a/apache2/modsecurity.h b/apache2/modsecurity.h index 6f94a856f8..e291bb4243 100644 --- a/apache2/modsecurity.h +++ b/apache2/modsecurity.h @@ -280,6 +280,8 @@ struct modsec_rec { apr_size_t reqbody_length; unsigned int reqbody_partial_processing_enabled; + unsigned int reqbody_length_limit_exceeded; + unsigned int reqbody_no_files_length_limit_exceeded; apr_bucket_brigade *of_brigade; unsigned int of_status; @@ -737,8 +739,6 @@ 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_multipart.c b/apache2/msc_multipart.c index 40c73587f7..fb23d7c972 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -19,10 +19,11 @@ #include "msc_multipart.h" #include "msc_util.h" #include "msc_parsers.h" -#include "dbg_print_bytes.h" +#include "msc_reqbody.h" #define CONTENT_TYPE_MAX_LENGTH 1024 +#include "dbg_print_bytes.h" #define DEBUG_BYTES_BUF_LEN 256 #define DEBUG_BYTES_OMIT_MARKER "...(snip)..." @@ -257,23 +258,6 @@ static int multipart_parse_content_disposition(modsec_rec *msr, char *c_d_value) return 1; } -/* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ -static void modsecurity_request_body_enable_partial_processing_for_no_files_length(modsec_rec *msr, - int *length) -{ - /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - *length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "MULTIPART: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); - } - modsecurity_request_body_enable_partial_processing(msr); - } -} - /** * */ @@ -295,12 +279,11 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { } /* The buffer is data so increase the data length counter. */ - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, - MULTIPART_BUF_SIZE - msr->mpd->bufleft, DEBUG_BYTES_OMIT_MARKER); + len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, len, "MULTIPART"); + msr->msc_reqbody_no_files_length += len; + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); msr_log(msr, 9, "[myDebug] Multipart: adding part_header, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, (MULTIPART_BUF_SIZE - msr->mpd->bufleft), debug_buf); - msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft); - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); + msr->msc_reqbody_no_files_length, len, debug_buf); if (len > 1) { if (msr->mpd->buf[len - 2] == '\r') { @@ -498,7 +481,7 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { assert(error_msg != NULL); char *p = msr->mpd->buf + (MULTIPART_BUF_SIZE - msr->mpd->bufleft); char localreserve[2] = { '\0', '\0' }; /* initialized to quiet warning */ - int bytes_reserved = 0; + int bytes_reserved = 0, len; char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -615,13 +598,12 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { value_part_t *value_part = apr_pcalloc(msr->mp, sizeof(value_part_t)); /* The buffer contains data so increase the data length counter. */ - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, - (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], DEBUG_BYTES_OMIT_MARKER); + len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, + (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], "MULTIPART"); + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); msr_log(msr, 9, "[myDebug] Multipart: adding part_form_data, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], debug_buf); - int len = (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; - msr->msc_reqbody_no_files_length += (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &msr->mpd->bufleft); + msr->msc_reqbody_no_files_length, len, debug_buf); + msr->msc_reqbody_no_files_length += len; /* add this part to the list of parts */ @@ -648,6 +630,8 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { if (msr->txcfg->debuglog_level >= 9) { msr_log(msr, 9, "Multipart: Added data to variable: %s", log_escape_nq_ex(msr->mp, value_part->data, value_part->length)); + dbg_print_bytes(debug_buf, sizeof(debug_buf), value_part->data, value_part->length, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] Multipart: Added data to variable: %s", debug_buf); } } else { diff --git a/apache2/msc_parsers.c b/apache2/msc_parsers.c index 30234c8d89..cc8a710585 100644 --- a/apache2/msc_parsers.c +++ b/apache2/msc_parsers.c @@ -15,6 +15,10 @@ #include "msc_parsers.h" #include +#include "dbg_print_bytes.h" +#define DEBUG_BYTES_BUF_LEN 256 +#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." + /** * */ @@ -355,6 +359,11 @@ void add_argument(modsec_rec *msr, apr_table_t *arguments, msc_arg *arg) msr_log(msr, 5, "Adding request argument (%s): name \"%s\", value \"%s\"", arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), log_escape_ex(msr->mp, arg->value, arg->value_len)); + char debug_buf[DEBUG_BYTES_BUF_LEN]; + dbg_print_bytes(debug_buf, sizeof(debug_buf), arg->value, arg->value_len, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 5, "[myDebug] Adding request argument (%s): name \"%s\", value \"%s\"", + arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), + debug_buf); } if (apr_table_elts(arguments)->nelts >= msr->txcfg->arguments_limit) { diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index f8ceed8043..06009aaf5b 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -18,6 +18,9 @@ #define CHUNK_CAPACITY 8192 +#include "dbg_print_bytes.h" +#define DEBUG_BYTES_BUF_LEN 256 +#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." /** * @@ -312,22 +315,53 @@ static apr_status_t modsecurity_request_body_store_memory(modsec_rec *msr, return 1; } -/* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ -static void modsecurity_request_body_enable_partial_processing_for_no_files_length(modsec_rec *msr, - apr_size_t *length, const char *reqbody_processor) +/** + * Enable partial processing of request body data. + */ +void modsecurity_request_body_do_enable_partial_processing(modsec_rec *msr) { + msr->reqbody_partial_processing_enabled = 1; + if (msr->msc_reqbody_processor == NULL) { + msr_log(msr, 9, "enable_partial_processing for none reqbody_processor"); + } + else 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"); + } + else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { + msr_log(msr, 4, "URLENCODED: Allow partial processing of request body"); + } +} + +/** + * Enable partial processing if no_files_len exceeds limit and action is ProcessPartial. + * Returns maybe adjusted adding_length. + */ +apr_ssize_t modsecurity_request_body_may_enable_partial_processing_for_no_files_length(modsec_rec *msr, + apr_ssize_t adding_length, const char *reqbody_processor) { /* Enable partial processing if no_files_len exceeds limit and action is ProcessPartial */ - if ( (msr->msc_reqbody_no_files_length > (unsigned long)msr->txcfg->reqbody_no_files_limit) - && (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL)) - { - *length -= msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit; - if (msr->txcfg->debuglog_level >= 9) { - msr_log(msr, 9, "%s: length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", - reqbody_processor, - msr->msc_reqbody_no_files_length - (unsigned long)msr->txcfg->reqbody_no_files_limit); + apr_ssize_t excess = (apr_ssize_t)msr->msc_reqbody_no_files_length + adding_length + - (apr_ssize_t)msr->txcfg->reqbody_no_files_limit; + if (excess > 0) { + msr->reqbody_no_files_length_limit_exceeded = 1; + if (msr->txcfg->if_limit_action == REQUEST_BODY_LIMIT_ACTION_PARTIAL) { + adding_length -= excess; + if (msr->txcfg->debuglog_level >= 9) { + msr_log(msr, 9, "%s: adding length shortened by %" APR_SIZE_T_FMT " bytes because of no_files_len limit.", + reqbody_processor, excess); + } + modsecurity_request_body_do_enable_partial_processing(msr); } - modsecurity_request_body_enable_partial_processing(msr); } + return adding_length; } /** @@ -378,8 +412,8 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } else if (strcmp(msr->msc_reqbody_processor, "XML") == 0) { /* Increase per-request data length counter. */ + length = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, length, "XML"); msr->msc_reqbody_no_files_length += length; - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "XML"); /* Process data as XML. */ if (xml_process_chunk(msr, data, length, &my_error_msg) < 0) { @@ -391,8 +425,8 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } else if (strcmp(msr->msc_reqbody_processor, "JSON") == 0) { /* Increase per-request data length counter. */ + length = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, length, "JSON"); msr->msc_reqbody_no_files_length += length; - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "JSON"); /* Process data as JSON. */ #ifdef WITH_YAJL @@ -411,8 +445,8 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } else if (strcmp(msr->msc_reqbody_processor, "URLENCODED") == 0) { /* Increase per-request data length counter. */ + length = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, length, "URLENCODED"); msr->msc_reqbody_no_files_length += length; - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "URLENCODED"); /* Do nothing else, URLENCODED processor does not support streaming. */ } @@ -423,12 +457,12 @@ apr_status_t modsecurity_request_body_store(modsec_rec *msr, } } else if (msr->txcfg->reqbody_buffering != REQUEST_BODY_FORCEBUF_OFF) { /* Increase per-request data length counter if forcing buffering. */ + length = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, length, "forceBuf"); msr->msc_reqbody_no_files_length += length; - modsecurity_request_body_enable_partial_processing_for_no_files_length(msr, &length, "forceBuf"); } /* 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->reqbody_no_files_length_limit_exceeded) { *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); if (msr->txcfg->debuglog_level >= 1) { @@ -459,31 +493,6 @@ 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) { - msr->reqbody_partial_processing_enabled = 1; - if (msr->msc_reqbody_processor == NULL) { - msr_log(msr, 9, "enable_partial_processing for none reqbody_processor"); - } - else 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"); - } - 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) { assert(msr != NULL); assert(error_msg != NULL); @@ -672,6 +681,7 @@ static apr_status_t modsecurity_request_body_end_urlencoded(modsec_rec *msr, cha assert(msr != NULL); assert(error_msg != NULL); int invalid_count = 0; + char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -682,7 +692,12 @@ static apr_status_t modsecurity_request_body_end_urlencoded(modsec_rec *msr, cha /* Parse URL-encoded arguments in the request body. */ - if (parse_arguments(msr, msr->msc_reqbody_buffer, msr->msc_reqbody_length, + unsigned int length = msr->msc_reqbody_length > msr->msc_reqbody_no_files_length + ? msr->msc_reqbody_no_files_length : msr->msc_reqbody_length; + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->msc_reqbody_buffer, length, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] modsecurity_request_body_end_urlencoded, length=%d, req_len=%d, no_files_len=%ld, buf=%s", + length, msr->msc_reqbody_length, msr->msc_reqbody_no_files_length, debug_buf); + if (parse_arguments(msr, msr->msc_reqbody_buffer, length, msr->txcfg->argument_separator, "BODY", msr->arguments, &invalid_count) < 0) { *error_msg = apr_pstrdup(msr->mp, "Initialisation: Error occurred while parsing BODY arguments."); @@ -717,7 +732,7 @@ apr_status_t modsecurity_request_body_end(modsec_rec *msr, char **error_msg) { /* 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->reqbody_no_files_length_limit_exceeded) { *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); if (msr->txcfg->debuglog_level >= 1) { diff --git a/apache2/msc_reqbody.h b/apache2/msc_reqbody.h new file mode 100644 index 0000000000..17fba52656 --- /dev/null +++ b/apache2/msc_reqbody.h @@ -0,0 +1,24 @@ +/* +* ModSecurity for Apache 2.x, http://www.modsecurity.org/ +* Copyright (c) 2004-2022 Trustwave Holdings, Inc. (http://www.trustwave.com/) +* +* You may not use this file except in compliance with +* the License.  You may obtain a copy of the License at +* +*     http://www.apache.org/licenses/LICENSE-2.0 +* +* If any of the files related to licensing are missing or if you have any +* other questions related to licensing please contact Trustwave Holdings, Inc. +* directly using the email address security@modsecurity.org. +*/ + +#ifndef _MSC_REQBODY_H_ +#define _MSC_REQBODY_H_ + +#include "modsecurity.h" + +void DSOLOCAL modsecurity_request_body_do_enable_partial_processing(modsec_rec *msr); +apr_ssize_t DSOLOCAL modsecurity_request_body_may_enable_partial_processing_for_no_files_length(modsec_rec *msr, + apr_ssize_t adding_length, const char *reqbody_processor); + +#endif diff --git a/iis/ModSecurityIIS.vcxproj b/iis/ModSecurityIIS.vcxproj index 7f2822a409..c61e30b7de 100755 --- a/iis/ModSecurityIIS.vcxproj +++ b/iis/ModSecurityIIS.vcxproj @@ -317,6 +317,7 @@ net start W3SVC /y + diff --git a/iis/ModSecurityIIS.vcxproj.filters b/iis/ModSecurityIIS.vcxproj.filters index cdc9bb8d90..764be53be1 100755 --- a/iis/ModSecurityIIS.vcxproj.filters +++ b/iis/ModSecurityIIS.vcxproj.filters @@ -159,6 +159,9 @@ ModSecurity + + ModSecurity + ModSecurity diff --git a/standalone/Makefile.am b/standalone/Makefile.am index c85d40ea65..34b5d2913c 100644 --- a/standalone/Makefile.am +++ b/standalone/Makefile.am @@ -1,164 +1,165 @@ -pkglibdir = $(prefix)/lib - -pkglib_LTLIBRARIES = standalone.la - -standalone_la_SOURCES = ../apache2/acmp.c \ - ../apache2/apache2_config.c \ - ../apache2/apache2_io.c \ - ../apache2/apache2_util.c \ - ../apache2/others/libinjection/src/libinjection_html5.c \ - ../apache2/others/libinjection/src/libinjection_sqli.c \ - ../apache2/others/libinjection/src/libinjection_xss.c \ - ../apache2/mod_security2.c \ - ../apache2/modsecurity.c \ - ../apache2/msc_status_engine.c \ - ../apache2/msc_crypt.c \ - ../apache2/msc_geo.c \ - ../apache2/msc_gsb.c \ - ../apache2/msc_json.c \ - ../apache2/msc_logging.c \ - ../apache2/msc_lua.c \ - ../apache2/msc_multipart.c \ - ../apache2/msc_parsers.c \ - ../apache2/msc_pcre.c \ - ../apache2/msc_release.c \ - ../apache2/msc_remote_rules.c \ - ../apache2/msc_reqbody.c \ - ../apache2/msc_tree.c \ - ../apache2/msc_unicode.c \ - ../apache2/msc_util.c \ - ../apache2/msc_xml.c \ - ../apache2/persist_dbm.c \ - ../apache2/re_actions.c \ - ../apache2/re.c \ - ../apache2/re_operators.c \ - ../apache2/re_tfns.c \ - ../apache2/re_variables.c \ - api.c \ - buckets.c \ - config.c \ - filters.c \ - hooks.c \ - regex.c \ - server.c - -standalone_la_CFLAGS = -DVERSION_STANDALONE \ - @APR_CFLAGS@ \ - @APU_CFLAGS@ \ - @APXS_CFLAGS@ \ - @CURL_CFLAGS@ \ - @LIBXML2_CFLAGS@ \ - @LUA_CFLAGS@ \ - @MODSEC_EXTRA_CFLAGS@ \ - @PCRE_CFLAGS@ \ - @YAJL_CFLAGS@ \ - @SSDEEP_CFLAGS@ - -standalone_la_CPPFLAGS = @APR_CPPFLAGS@ \ - @LIBXML2_CPPFLAGS@ \ - @PCRE_CPPFLAGS@ - -standalone_la_LIBADD = @APR_LDADD@ \ - @APU_LDADD@ \ - @LIBXML2_LDADD@ \ - @LUA_LDADD@ \ - @PCRE_LDADD@ \ - @YAJL_LDADD@ \ - @SSDEEP_CFLAGS@ - -if AIX -standalone_la_LDFLAGS = -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if HPUX -standalone_la_LDFLAGS = -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if MACOSX -standalone_la_LDFLAGS = -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if SOLARIS -standalone_la_LDFLAGS = -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if LINUX -standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if FREEBSD -standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if OPENBSD -standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - -if NETBSD -standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ - @APR_LDFLAGS@ \ - @APU_LDFLAGS@ \ - @APXS_LDFLAGS@ \ - @LIBXML2_LDFLAGS@ \ - @LUA_LDFLAGS@ \ - @PCRE_LDFLAGS@ \ - @YAJL_LDFLAGS@ \ - @SSDEEP_LDFLAGS@ -endif - +pkglibdir = $(prefix)/lib + +pkglib_LTLIBRARIES = standalone.la + +standalone_la_SOURCES = ../apache2/acmp.c \ + ../apache2/apache2_config.c \ + ../apache2/apache2_io.c \ + ../apache2/apache2_util.c \ + ../apache2/dbg_print_bytes.c \ + ../apache2/others/libinjection/src/libinjection_html5.c \ + ../apache2/others/libinjection/src/libinjection_sqli.c \ + ../apache2/others/libinjection/src/libinjection_xss.c \ + ../apache2/mod_security2.c \ + ../apache2/modsecurity.c \ + ../apache2/msc_status_engine.c \ + ../apache2/msc_crypt.c \ + ../apache2/msc_geo.c \ + ../apache2/msc_gsb.c \ + ../apache2/msc_json.c \ + ../apache2/msc_logging.c \ + ../apache2/msc_lua.c \ + ../apache2/msc_multipart.c \ + ../apache2/msc_parsers.c \ + ../apache2/msc_pcre.c \ + ../apache2/msc_release.c \ + ../apache2/msc_remote_rules.c \ + ../apache2/msc_reqbody.c \ + ../apache2/msc_tree.c \ + ../apache2/msc_unicode.c \ + ../apache2/msc_util.c \ + ../apache2/msc_xml.c \ + ../apache2/persist_dbm.c \ + ../apache2/re_actions.c \ + ../apache2/re.c \ + ../apache2/re_operators.c \ + ../apache2/re_tfns.c \ + ../apache2/re_variables.c \ + api.c \ + buckets.c \ + config.c \ + filters.c \ + hooks.c \ + regex.c \ + server.c + +standalone_la_CFLAGS = -DVERSION_STANDALONE \ + @APR_CFLAGS@ \ + @APU_CFLAGS@ \ + @APXS_CFLAGS@ \ + @CURL_CFLAGS@ \ + @LIBXML2_CFLAGS@ \ + @LUA_CFLAGS@ \ + @MODSEC_EXTRA_CFLAGS@ \ + @PCRE_CFLAGS@ \ + @YAJL_CFLAGS@ \ + @SSDEEP_CFLAGS@ + +standalone_la_CPPFLAGS = @APR_CPPFLAGS@ \ + @LIBXML2_CPPFLAGS@ \ + @PCRE_CPPFLAGS@ + +standalone_la_LIBADD = @APR_LDADD@ \ + @APU_LDADD@ \ + @LIBXML2_LDADD@ \ + @LUA_LDADD@ \ + @PCRE_LDADD@ \ + @YAJL_LDADD@ \ + @SSDEEP_CFLAGS@ + +if AIX +standalone_la_LDFLAGS = -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if HPUX +standalone_la_LDFLAGS = -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if MACOSX +standalone_la_LDFLAGS = -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if SOLARIS +standalone_la_LDFLAGS = -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if LINUX +standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if FREEBSD +standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if OPENBSD +standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + +if NETBSD +standalone_la_LDFLAGS = -no-undefined -module -avoid-version \ + @APR_LDFLAGS@ \ + @APU_LDFLAGS@ \ + @APXS_LDFLAGS@ \ + @LIBXML2_LDFLAGS@ \ + @LUA_LDFLAGS@ \ + @PCRE_LDFLAGS@ \ + @YAJL_LDFLAGS@ \ + @SSDEEP_LDFLAGS@ +endif + diff --git a/standalone/modules.mk b/standalone/modules.mk index 048f33bc84..592f869b68 100644 --- a/standalone/modules.mk +++ b/standalone/modules.mk @@ -4,7 +4,7 @@ MOD_SECURITY2 = mod_security2 apache2_config apache2_io apache2_util \ persist_dbm msc_reqbody pdf_protect msc_geo msc_gsb msc_unicode acmp msc_lua H = re.h modsecurity.h msc_logging.h msc_multipart.h msc_parsers.h \ - msc_pcre.h msc_util.h msc_xml.h persist_dbm.h apache2.h pdf_protect.h \ + msc_pcre.h msc_util.h msc_xml.h persist_dbm.h msc_reqbody.h apache2.h pdf_protect.h \ msc_geo.h msc_gsb.h msc_unicode.h acmp.h utf8tables.h msc_lua.h ${MOD_SECURITY2:=.slo}: ${H} diff --git a/standalone/standalone.vcxproj.filters b/standalone/standalone.vcxproj.filters index f51478e8f5..2dadf4433e 100644 --- a/standalone/standalone.vcxproj.filters +++ b/standalone/standalone.vcxproj.filters @@ -161,6 +161,9 @@ ModSecurity Headers + + ModSecurity Headers + ModSecurity Headers From c4e87ed89378d6c91c033f8e27ca0525048a5c4b Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 3 Jul 2026 14:52:28 +0900 Subject: [PATCH 39/41] Fix multipart parser for handling SecRequestBodyNoFilesLimit --- apache2/msc_multipart.c | 26 ++++++++++--------- ...0-reqbody-limit-action-multipart-chunked.t | 4 +-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index fb23d7c972..bbbacf32ec 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -280,10 +280,12 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { /* The buffer is data so increase the data length counter. */ len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, len, "MULTIPART"); + if (msr->txcfg->debuglog_level >= 9) { + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] Multipart: adding part_header, no_files_len=%lu, add_len=%d, buf=%s", + msr->msc_reqbody_no_files_length, len, debug_buf); + } msr->msc_reqbody_no_files_length += len; - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] Multipart: adding part_header, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, len, debug_buf); if (len > 1) { if (msr->mpd->buf[len - 2] == '\r') { @@ -600,9 +602,11 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { /* The buffer contains data so increase the data length counter. */ len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], "MULTIPART"); - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] Multipart: adding part_form_data, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, len, debug_buf); + if (msr->txcfg->debuglog_level >= 9) { + dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); + msr_log(msr, 9, "[myDebug] Multipart: adding part_form_data, no_files_len=%lu, add_len=%d, buf=%s", + msr->msc_reqbody_no_files_length, len, debug_buf); + } msr->msc_reqbody_no_files_length += len; /* add this part to the list of parts */ @@ -613,14 +617,14 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { } if (msr->mpd->reserve[0] != 0) { - value_part->data = apr_palloc(msr->mp, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]); + value_part->data = apr_palloc(msr->mp, len); memcpy(value_part->data, &(msr->mpd->reserve[1]), msr->mpd->reserve[0]); - memcpy(value_part->data + msr->mpd->reserve[0], msr->mpd->buf, (MULTIPART_BUF_SIZE - msr->mpd->bufleft)); + memcpy(value_part->data + msr->mpd->reserve[0], msr->mpd->buf, len - msr->mpd->reserve[0]); - value_part->length = (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0]; + value_part->length = len; msr->mpd->mpp->length += value_part->length; } else { - value_part->length = (MULTIPART_BUF_SIZE - msr->mpd->bufleft); + value_part->length = len; value_part->data = apr_pstrmemdup(msr->mp, msr->mpd->buf, value_part->length); msr->mpd->mpp->length += value_part->length; } @@ -630,8 +634,6 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { if (msr->txcfg->debuglog_level >= 9) { msr_log(msr, 9, "Multipart: Added data to variable: %s", log_escape_nq_ex(msr->mp, value_part->data, value_part->length)); - dbg_print_bytes(debug_buf, sizeof(debug_buf), value_part->data, value_part->length, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] Multipart: Added data to variable: %s", debug_buf); } } else { diff --git a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t index f05690eeda..ef411cefc0 100644 --- a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t +++ b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t @@ -704,7 +704,7 @@ ), match_log => { error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], - debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], + debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { status => qr/^403$/, @@ -729,7 +729,7 @@ -----------------------------69343412719991675451336310646 Content-Disposition: form-data; name="b" - ) . 'b' x 16270 . q(bad_value + ) . 'b' x 16269 . q(bad_valueb -----------------------------69343412719991675451336310646-- ) ), From dd01f1f073c517691821a1897722ddcfe5c41142 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 3 Jul 2026 15:13:26 +0900 Subject: [PATCH 40/41] Remove debug prints --- apache2/Makefile.am | 1 - apache2/dbg_print_bytes.c | 153 -------------------------------------- apache2/dbg_print_bytes.h | 28 ------- apache2/msc_multipart.c | 16 ---- apache2/msc_parsers.c | 9 --- apache2/msc_reqbody.c | 8 -- standalone/Makefile.am | 1 - 7 files changed, 216 deletions(-) delete mode 100644 apache2/dbg_print_bytes.c delete mode 100644 apache2/dbg_print_bytes.h diff --git a/apache2/Makefile.am b/apache2/Makefile.am index f6971abfdd..11b6c69e1a 100644 --- a/apache2/Makefile.am +++ b/apache2/Makefile.am @@ -5,7 +5,6 @@ mod_security2_la_SOURCES = acmp.c \ apache2_config.c \ apache2_io.c \ apache2_util.c \ - dbg_print_bytes.c \ others/libinjection/src/libinjection_html5.c \ others/libinjection/src/libinjection_sqli.c \ others/libinjection/src/libinjection_xss.c \ diff --git a/apache2/dbg_print_bytes.c b/apache2/dbg_print_bytes.c deleted file mode 100644 index 7d71fd87ec..0000000000 --- a/apache2/dbg_print_bytes.c +++ /dev/null @@ -1,153 +0,0 @@ -#include "dbg_print_bytes.h" -#include -#include - -/* ---- internal helpers ---- */ - -/** - * Return non-zero if byte c needs escaping in the output. - * Backslash (0x5C) is escaped as \\ to avoid ambiguity - * with escape sequences. Other non-printable/control/high bytes - * are escaped as \xNN. - */ -static int needs_escape(unsigned char c) { - return !(0x20 <= c && c <= 0x7E) || c == '\\'; -} - -/** - * Escaped output length for byte c: - * 1 when kept as-is (safe printable) - * 2 for backslash (escaped as \\) - * 4 otherwise (escaped as \xNN) - */ -static size_t esc_len(unsigned char c) { - if (c == '\\') return 2; - return needs_escape(c) ? 4 : 1; -} - -/** - * Write byte c into dest[pos..] with proper escaping. - * Returns the number of bytes written. - */ -static size_t write_byte(char *dest, size_t pos, unsigned char c) { - if (!needs_escape(c)) { - dest[pos] = (char)c; - return 1; - } - if (c == '\\') { - dest[pos + 0] = '\\'; - dest[pos + 1] = '\\'; - return 2; - } - /* \xNN (lowercase) */ - static const char hex[] = "0123456789abcdef"; - dest[pos + 0] = '\\'; - dest[pos + 1] = 'x'; - dest[pos + 2] = hex[c >> 4]; - dest[pos + 3] = hex[c & 0x0f]; - return 4; -} - -/** - * Escape and write src[0..len) into dest starting at pos. - * Returns the new position after writing. - */ -static size_t write_bytes(char *dest, size_t pos, - const unsigned char *src, size_t len) { - for (size_t i = 0; i < len; i++) { - pos += write_byte(dest, pos, src[i]); - } - return pos; -} - -/* ---- public API ---- */ - -size_t dbg_print_bytes(char *dest, size_t dest_size, - const unsigned char *src, size_t src_len, - const char *omit) -{ - /* error / empty input */ - if (dest == NULL || dest_size == 0) { - return 0; - } - if (src == NULL || src_len == 0) { - dest[0] = '\0'; - return 0; - } - - /* compute total escaped length */ - size_t total = 0; - for (size_t i = 0; i < src_len; i++) { - total += esc_len(src[i]); - } - - const size_t max_out = dest_size - 1; /* reserve room for null terminator */ - - /* ---- fits without truncation ---- */ - if (total <= max_out) { - size_t pos = write_bytes(dest, 0, src, src_len); - dest[pos] = '\0'; - return total; - } - - /* ---- truncation needed ---- */ - const char *mid = omit ? omit : "..."; - size_t mid_len = strlen(mid); - size_t head_len, tail_len; - int use_mid = 1; /* 1: include marker, 0: omit marker */ - - if (max_out < mid_len + 2) { - /* not enough room for marker + at least 2 chars */ - use_mid = 0; - head_len = max_out; - tail_len = 0; - } else { - head_len = (max_out - mid_len + 1) / 2; /* round up */ - tail_len = (max_out - mid_len) - head_len; /* remainder */ - } - - /* --- determine head range (up to head_len escaped chars from start) --- */ - size_t head_end = 0; /* index into src (exclusive) */ - { - size_t acc = 0; - for (size_t i = 0; i < src_len; i++) { - size_t len = esc_len(src[i]); - if (acc + len > head_len) break; - acc += len; - head_end = i + 1; - } - } - - /* --- determine tail range (up to tail_len escaped chars from end) --- */ - size_t tail_start = src_len; /* index into src (start of tail portion) */ - if (tail_len > 0) { - size_t acc = 0; - for (size_t i = src_len; i > 0; i--) { - size_t len = esc_len(src[i - 1]); - if (acc + len > tail_len) break; - acc += len; - tail_start = i - 1; - } - } - - /* --- write to output buffer --- */ - size_t pos = 0; - - /* head portion */ - pos = write_bytes(dest, pos, src, head_end); - - /* middle omission marker (only if there is a gap between head and tail) */ - if (use_mid && head_end < tail_start) { - memcpy(dest + pos, mid, mid_len); - pos += mid_len; - } - - /* tail portion */ - if (tail_start < src_len) { - pos = write_bytes(dest, pos, src + tail_start, - src_len - tail_start); - } - - dest[pos] = '\0'; - return total; -} diff --git a/apache2/dbg_print_bytes.h b/apache2/dbg_print_bytes.h deleted file mode 100644 index c5fc030c9c..0000000000 --- a/apache2/dbg_print_bytes.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef DBG_PRINT_BYTES_H -#define DBG_PRINT_BYTES_H - -#include - -/** - * @brief Format an arbitrary byte sequence for debug output into a fixed-size buffer - * - * Bytes in the safe printable range (0x20-0x7E except backslash 0x5C) are output - * as-is. Backslash is escaped to \\ to avoid ambiguity. All other bytes are - * escaped in \xNN format (lowercase hexadecimal, zero-padded). - * If the escaped output does not fit in the buffer, the middle part is omitted - * and replaced with the omission marker, keeping the head and tail portions. - * - * @param dest Output buffer. If NULL, returns 0 without doing anything. - * @param dest_size Size of the output buffer in bytes. If 0, returns 0. - * @param src Input byte sequence. If NULL or src_len is 0, treated as empty. - * @param src_len Length of the input byte sequence in bytes. - * @param omit Omission marker string inserted between head and tail when - * truncation occurs. If NULL, defaults to "...". - * @return The total length of the fully escaped string (excluding null terminator). - * If the return value is >= dest_size, truncation (omission) occurred. - */ -size_t dbg_print_bytes(char *dest, size_t dest_size, - const unsigned char *src, size_t src_len, - const char *omit); - -#endif /* DBG_PRINT_BYTES_H */ diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c index bbbacf32ec..14fe1e0505 100644 --- a/apache2/msc_multipart.c +++ b/apache2/msc_multipart.c @@ -23,10 +23,6 @@ #define CONTENT_TYPE_MAX_LENGTH 1024 -#include "dbg_print_bytes.h" -#define DEBUG_BYTES_BUF_LEN 256 -#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." - void validate_quotes(modsec_rec *msr, char *data, char quote) { assert(msr != NULL); int i, len; @@ -265,7 +261,6 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { assert(msr != NULL); assert(error_msg != NULL); int i, len, rc; - char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -280,11 +275,6 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { /* The buffer is data so increase the data length counter. */ len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, len, "MULTIPART"); - if (msr->txcfg->debuglog_level >= 9) { - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] Multipart: adding part_header, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, len, debug_buf); - } msr->msc_reqbody_no_files_length += len; if (len > 1) { @@ -484,7 +474,6 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { char *p = msr->mpd->buf + (MULTIPART_BUF_SIZE - msr->mpd->bufleft); char localreserve[2] = { '\0', '\0' }; /* initialized to quiet warning */ int bytes_reserved = 0, len; - char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -602,11 +591,6 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { /* The buffer contains data so increase the data length counter. */ len = modsecurity_request_body_may_enable_partial_processing_for_no_files_length(msr, (MULTIPART_BUF_SIZE - msr->mpd->bufleft) + msr->mpd->reserve[0], "MULTIPART"); - if (msr->txcfg->debuglog_level >= 9) { - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->mpd->buf, len, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] Multipart: adding part_form_data, no_files_len=%lu, add_len=%d, buf=%s", - msr->msc_reqbody_no_files_length, len, debug_buf); - } msr->msc_reqbody_no_files_length += len; /* add this part to the list of parts */ diff --git a/apache2/msc_parsers.c b/apache2/msc_parsers.c index cc8a710585..30234c8d89 100644 --- a/apache2/msc_parsers.c +++ b/apache2/msc_parsers.c @@ -15,10 +15,6 @@ #include "msc_parsers.h" #include -#include "dbg_print_bytes.h" -#define DEBUG_BYTES_BUF_LEN 256 -#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." - /** * */ @@ -359,11 +355,6 @@ void add_argument(modsec_rec *msr, apr_table_t *arguments, msc_arg *arg) msr_log(msr, 5, "Adding request argument (%s): name \"%s\", value \"%s\"", arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), log_escape_ex(msr->mp, arg->value, arg->value_len)); - char debug_buf[DEBUG_BYTES_BUF_LEN]; - dbg_print_bytes(debug_buf, sizeof(debug_buf), arg->value, arg->value_len, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 5, "[myDebug] Adding request argument (%s): name \"%s\", value \"%s\"", - arg->origin, log_escape_ex(msr->mp, arg->name, arg->name_len), - debug_buf); } if (apr_table_elts(arguments)->nelts >= msr->txcfg->arguments_limit) { diff --git a/apache2/msc_reqbody.c b/apache2/msc_reqbody.c index 06009aaf5b..82b4bc3d79 100644 --- a/apache2/msc_reqbody.c +++ b/apache2/msc_reqbody.c @@ -18,10 +18,6 @@ #define CHUNK_CAPACITY 8192 -#include "dbg_print_bytes.h" -#define DEBUG_BYTES_BUF_LEN 256 -#define DEBUG_BYTES_OMIT_MARKER "...(snip)..." - /** * */ @@ -681,7 +677,6 @@ static apr_status_t modsecurity_request_body_end_urlencoded(modsec_rec *msr, cha assert(msr != NULL); assert(error_msg != NULL); int invalid_count = 0; - char debug_buf[DEBUG_BYTES_BUF_LEN]; *error_msg = NULL; @@ -694,9 +689,6 @@ static apr_status_t modsecurity_request_body_end_urlencoded(modsec_rec *msr, cha unsigned int length = msr->msc_reqbody_length > msr->msc_reqbody_no_files_length ? msr->msc_reqbody_no_files_length : msr->msc_reqbody_length; - dbg_print_bytes(debug_buf, sizeof(debug_buf), msr->msc_reqbody_buffer, length, DEBUG_BYTES_OMIT_MARKER); - msr_log(msr, 9, "[myDebug] modsecurity_request_body_end_urlencoded, length=%d, req_len=%d, no_files_len=%ld, buf=%s", - length, msr->msc_reqbody_length, msr->msc_reqbody_no_files_length, debug_buf); if (parse_arguments(msr, msr->msc_reqbody_buffer, length, msr->txcfg->argument_separator, "BODY", msr->arguments, &invalid_count) < 0) { diff --git a/standalone/Makefile.am b/standalone/Makefile.am index 34b5d2913c..bb44437d9d 100644 --- a/standalone/Makefile.am +++ b/standalone/Makefile.am @@ -6,7 +6,6 @@ standalone_la_SOURCES = ../apache2/acmp.c \ ../apache2/apache2_config.c \ ../apache2/apache2_io.c \ ../apache2/apache2_util.c \ - ../apache2/dbg_print_bytes.c \ ../apache2/others/libinjection/src/libinjection_html5.c \ ../apache2/others/libinjection/src/libinjection_sqli.c \ ../apache2/others/libinjection/src/libinjection_xss.c \ From 8ba376ff468c4e666aa1b1466d1e8e7fb660ac86 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Fri, 3 Jul 2026 16:15:19 +0900 Subject: [PATCH 41/41] Remove references of REQBODY_PROCESSOR_ERROR in regression test --- ...0-reqbody-limit-action-multipart-chunked.t | 62 +++++++------------ 1 file changed, 24 insertions(+), 38 deletions(-) diff --git a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t index ef411cefc0..e06f279ed0 100644 --- a/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t +++ b/tests/regression/config/10-reqbody-limit-action-multipart-chunked.t @@ -13,7 +13,7 @@ SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ + RE %{REQBODY_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -31,7 +31,7 @@ ) ), match_log => { - error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + error => [ qr/Multipart flags: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -79,7 +79,7 @@ SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ + RE %{REQBODY_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -97,7 +97,7 @@ ) ), match_log => { - error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + error => [ qr/Multipart flags: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -145,7 +145,7 @@ SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ + RE %{REQBODY_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -163,7 +163,7 @@ ) ), match_log => { - error => [ qr/Multipart flags: PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], + error => [ qr/Multipart flags: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0./, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -211,7 +211,7 @@ SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ + RE %{REQBODY_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -277,7 +277,7 @@ SecRule REQBODY_PROCESSOR "^MULTIPART\$" \\ "id:'200000',phase:2,t:none,log, \\ msg:'Multipart flags: \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ + RE %{REQBODY_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -344,7 +344,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -363,7 +362,7 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -412,7 +411,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -431,7 +429,7 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -480,7 +478,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -499,7 +496,7 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Request body no files length: 16384/, 1], }, match_response => { @@ -548,7 +545,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -567,7 +563,7 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], }, match_response => { @@ -616,7 +612,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -635,7 +630,7 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], }, match_response => { @@ -684,7 +679,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -703,8 +697,8 @@ 4096 ), match_log => { - error => [ qr/heck values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], - debug => [ qr/Request body no files length: 16384/, 1], + error => [ qr/heck values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + debug => [ qr/Request body no files data length is larger than the configured limit \(16384\)\./, 1], }, match_response => { status => qr/^403$/, @@ -752,7 +746,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -770,7 +763,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 1], }, match_response => { @@ -819,7 +812,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -837,7 +829,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 1], }, match_response => { @@ -886,7 +878,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -904,7 +895,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Input filter: Completed receiving request body \(length 32768\)\./, 1], }, match_response => { @@ -953,7 +944,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -971,7 +961,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Multipart: Allow partial processing of request body/, 1], }, match_response => { @@ -1020,7 +1010,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -1038,7 +1027,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Multipart: Allow partial processing of request body/, 1], }, match_response => { @@ -1086,7 +1075,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -1104,7 +1092,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Multipart: Allow partial processing of request body/, 1], }, match_response => { @@ -1152,7 +1140,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -1170,7 +1157,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Multipart: Allow partial processing of request body/, 1], }, match_response => { @@ -1196,7 +1183,7 @@ -----------------------------69343412719991675451336310646 Content-Disposition: form-data; name="b" - ) . "b" x 16273 . q(ad_value + ) . "b" x 16270 . q(ad_valueb -----------------------------69343412719991675451336310646--) ), 4096 @@ -1218,7 +1205,6 @@ "id:'200000',phase:2,t:none,log, \\ msg:'Check values for test: \\ RE %{REQBODY_ERROR}, \\ - PE %{REQBODY_PROCESSOR_ERROR}, \\ BQ %{MULTIPART_BOUNDARY_QUOTED}, \\ BW %{MULTIPART_BOUNDARY_WHITESPACE}, \\ DB %{MULTIPART_DATA_BEFORE}, \\ @@ -1236,7 +1222,7 @@ ) ), match_log => { - error => [ qr/Check values for test: RE 0, PE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], + error => [ qr/Check values for test: RE 0, BQ 0, BW 0, DB 0, DA 0, HF 0, LF 0, SM 0, IQ 0, IP 0, IH 0, FL 0, UB 0/, 1], debug => [ qr/Multipart: Allow partial processing of request body/, 1], }, match_response => {