diff --git a/CVE-2022-48279.patch b/CVE-2022-48279.patch new file mode 100644 index 0000000000000000000000000000000000000000..ee54388eb8f96c27d63a656beac03e9904c3b545 --- /dev/null +++ b/CVE-2022-48279.patch @@ -0,0 +1,405 @@ +From 7a489bd07c66d3df19a320b4306e00c49716dbb0 Mon Sep 17 00:00:00 2001 +From: Martin Vierula +Date: Wed, 7 Sep 2022 11:09:47 -0700 +Subject: [PATCH] Multipart parsing fixes and new MULTIPART_PART_HEADERS + collection + +--- + CHANGES | 2 + + apache2/msc_multipart.c | 148 ++++++++++++++------ + apache2/msc_multipart.h | 19 +++ + apache2/re_variables.c | 57 ++++++++ + tests/regression/misc/00-multipart-parser.t | 45 ++++++ + 5 files changed, 230 insertions(+), 41 deletions(-) + +diff --git a/apache2/msc_multipart.c b/apache2/msc_multipart.c +index d087c863e..4128ab17e 100644 +--- a/apache2/msc_multipart.c ++++ b/apache2/msc_multipart.c +@@ -325,7 +325,14 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { + } + + msr->mpd->mpp_state = 1; ++ msr->mpd->mpp_substate_part_data_read = 0; + msr->mpd->mpp->last_header_name = NULL; ++ ++ /* Record the last part header line in the collection */ ++ if (msr->mpd->mpp->last_header_line != NULL) { ++ *(char **)apr_array_push(msr->mpd->mpp->header_lines) = msr->mpd->mpp->last_header_line; ++ msr_log(msr, 9, "Multipart: Added part header line \"%s\"", msr->mpd->mpp->last_header_line); ++ } + } else { + /* Header line. */ + +@@ -379,12 +386,28 @@ static int multipart_process_part_header(modsec_rec *msr, char **error_msg) { + *error_msg = apr_psprintf(msr->mp, "Multipart: Part header too long."); + return -1; + } ++ if ((msr->mpd->mpp->last_header_line != NULL) && (msr->mpd->mpp->last_header_name != NULL) ++ && (new_value != NULL)) { ++ msr->mpd->mpp->last_header_line = apr_psprintf(msr->mp, ++ "%s: %s", msr->mpd->mpp->last_header_name, new_value); ++ } ++ + } else { + char *header_name, *header_value, *data; + + /* new header */ + ++ /* Record the most recently-seen part header line in the collection */ ++ if (msr->mpd->mpp->last_header_line != NULL) { ++ *(char **)apr_array_push(msr->mpd->mpp->header_lines) = msr->mpd->mpp->last_header_line; ++ msr_log(msr, 9, "Multipart: Added part header line \"%s\"", msr->mpd->mpp->last_header_line); ++ } ++ + data = msr->mpd->buf; ++ ++ msr->mpd->mpp->last_header_line = apr_pstrdup(msr->mp, data); ++ remove_lf_crlf_inplace(msr->mpd->mpp->last_header_line); ++ + while((*data != ':') && (*data != '\0')) data++; + if (*data == '\0') { + *error_msg = apr_psprintf(msr->mp, "Multipart: Invalid part header (colon missing): %s.", +@@ -438,6 +461,8 @@ static int multipart_process_part_data(modsec_rec *msr, char **error_msg) { + if (error_msg == NULL) return -1; + *error_msg = NULL; + ++ msr->mpd->mpp_substate_part_data_read = 1; ++ + /* Preserve some bytes for later. */ + if ( ((MULTIPART_BUF_SIZE - msr->mpd->bufleft) >= 1) + && (*(p - 1) == '\n') ) +@@ -680,10 +705,14 @@ static int multipart_process_boundary(modsec_rec *msr, int last_part, char **err + if (msr->mpd->mpp == NULL) return -1; + msr->mpd->mpp->type = MULTIPART_FORMDATA; + msr->mpd->mpp_state = 0; ++ msr->mpd->mpp_substate_part_data_read = 0; + + msr->mpd->mpp->headers = apr_table_make(msr->mp, 10); + if (msr->mpd->mpp->headers == NULL) return -1; + msr->mpd->mpp->last_header_name = NULL; ++ msr->mpd->mpp->last_header_line = NULL; ++ msr->mpd->mpp->header_lines = apr_array_make(msr->mp, 2, sizeof(char *)); ++ if (msr->mpd->mpp->header_lines == NULL) return -1; + + msr->mpd->reserve[0] = 0; + msr->mpd->reserve[1] = 0; +@@ -983,6 +1012,19 @@ int multipart_complete(modsec_rec *msr, char **error_msg) { + && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary)) == '-') + && (*(msr->mpd->buf + 2 + strlen(msr->mpd->boundary) + 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->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; +@@ -1075,54 +1117,63 @@ int multipart_process_chunk(modsec_rec *msr, const char *buf, + if ( (strlen(msr->mpd->buf) >= strlen(msr->mpd->boundary) + 2) + && (strncmp(msr->mpd->buf + 2, msr->mpd->boundary, strlen(msr->mpd->boundary)) == 0) ) + { +- char *boundary_end = msr->mpd->buf + 2 + strlen(msr->mpd->boundary); +- int is_final = 0; ++ if (msr->mpd->crlf_state_buf_end == 2) { ++ msr->mpd->flag_lf_line = 1; ++ } ++ if ((msr->mpd->mpp_substate_part_data_read == 0) && (msr->mpd->boundary_count > 0)) { ++ /* string matches our 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 boundary)"); ++ } else { ++ char *boundary_end = msr->mpd->buf + 2 + strlen(msr->mpd->boundary); ++ int is_final = 0; ++ ++ /* Is this the final boundary? */ ++ if ((*boundary_end == '-') && (*(boundary_end + 1)== '-')) { ++ is_final = 1; ++ boundary_end += 2; ++ ++ if (msr->mpd->is_complete != 0) { ++ msr->mpd->flag_error = 1; ++ *error_msg = apr_psprintf(msr->mp, ++ "Multipart: Invalid boundary (final duplicate)."); ++ return -1; ++ } ++ } + +- /* Is this the final boundary? */ +- if ((*boundary_end == '-') && (*(boundary_end + 1)== '-')) { +- is_final = 1; +- boundary_end += 2; ++ /* Allow for CRLF and LF line endings. */ ++ if ( ( (*boundary_end == '\r') ++ && (*(boundary_end + 1) == '\n') ++ && (*(boundary_end + 2) == '\0') ) ++ || ( (*boundary_end == '\n') ++ && (*(boundary_end + 1) == '\0') ) ) ++ { ++ if (*boundary_end == '\n') { ++ msr->mpd->flag_lf_line = 1; ++ } else { ++ msr->mpd->flag_crlf_line = 1; ++ } + +- if (msr->mpd->is_complete != 0) { +- msr->mpd->flag_error = 1; +- *error_msg = apr_psprintf(msr->mp, +- "Multipart: Invalid boundary (final duplicate)."); +- return -1; +- } +- } ++ if (multipart_process_boundary(msr, (is_final ? 1 : 0), error_msg) < 0) { ++ msr->mpd->flag_error = 1; ++ return -1; ++ } + +- /* Allow for CRLF and LF line endings. */ +- if ( ( (*boundary_end == '\r') +- && (*(boundary_end + 1) == '\n') +- && (*(boundary_end + 2) == '\0') ) +- || ( (*boundary_end == '\n') +- && (*(boundary_end + 1) == '\0') ) ) +- { +- if (*boundary_end == '\n') { +- msr->mpd->flag_lf_line = 1; +- } else { +- msr->mpd->flag_crlf_line = 1; +- } ++ if (is_final) { ++ msr->mpd->is_complete = 1; ++ } + +- if (multipart_process_boundary(msr, (is_final ? 1 : 0), error_msg) < 0) { ++ processed_as_boundary = 1; ++ msr->mpd->boundary_count++; ++ } ++ else { ++ /* error */ + msr->mpd->flag_error = 1; ++ *error_msg = apr_psprintf(msr->mp, ++ "Multipart: Invalid boundary: %s", ++ log_escape_nq(msr->mp, msr->mpd->buf)); + return -1; + } +- +- if (is_final) { +- msr->mpd->is_complete = 1; +- } +- +- processed_as_boundary = 1; +- msr->mpd->boundary_count++; +- } +- else { +- /* error */ +- msr->mpd->flag_error = 1; +- *error_msg = apr_psprintf(msr->mp, +- "Multipart: Invalid boundary: %s", +- log_escape_nq(msr->mp, msr->mpd->buf)); +- return -1; + } + } else { /* It looks like a boundary but we couldn't match it. */ + char *p = NULL; +@@ -1221,6 +1272,21 @@ int multipart_process_chunk(modsec_rec *msr, const char *buf, + msr->mpd->bufptr = msr->mpd->buf; + msr->mpd->bufleft = MULTIPART_BUF_SIZE; + msr->mpd->buf_contains_line = (c == 0x0a) ? 1 : 0; ++ ++ if (c == 0x0a) { ++ if (msr->mpd->crlf_state == 1) { ++ msr->mpd->crlf_state = 3; ++ } else { ++ msr->mpd->crlf_state = 2; ++ } ++ } ++ msr->mpd->crlf_state_buf_end = msr->mpd->crlf_state; ++ } ++ ++ if (c == 0x0d) { ++ msr->mpd->crlf_state = 1; ++ } else if (c != 0x0a) { ++ msr->mpd->crlf_state = 0; + } + + if ((msr->mpd->is_complete) && (inleft != 0)) { +diff --git a/apache2/msc_multipart.h b/apache2/msc_multipart.h +index a0f6a08dd..13db0658f 100644 +--- a/apache2/msc_multipart.h ++++ b/apache2/msc_multipart.h +@@ -55,6 +55,8 @@ struct multipart_part { + + char *last_header_name; + apr_table_t *headers; ++ char *last_header_line; ++ apr_array_header_t *header_lines; + + unsigned int offset; + unsigned int length; +@@ -81,6 +83,15 @@ struct multipart_data { + char *bufptr; + int bufleft; + ++ /* line ending status seen immediately before current position. ++ * 0 = neither LF nor CR; 1 = prev char CR; 2 = prev char LF alone; ++ * 3 = prev two chars were CRLF ++ */ ++ int crlf_state; ++ ++ /* crlf_state at end of previous buffer */ ++ int crlf_state_buf_end; ++ + unsigned int buf_offset; + + /* pointer that keeps track of a part while +@@ -94,6 +105,14 @@ struct multipart_data { + */ + int mpp_state; + ++ /* part parsing substate; if mpp_state is 1 (collecting ++ * data), then for this variable: ++ * 0 means we have not yet read any data between the ++ * post-headers blank line and the next boundary ++ * 1 means we have read at some data after that blank line ++ */ ++ int mpp_substate_part_data_read; ++ + /* because of the way this parsing algorithm + * works we hold back the last two bytes of + * each data chunk so that we can discard it +diff --git a/apache2/re_variables.c b/apache2/re_variables.c +index 400738615..f3015acd9 100644 +--- a/apache2/re_variables.c ++++ b/apache2/re_variables.c +@@ -1394,6 +1394,52 @@ static int var_files_combined_size_generate(modsec_rec *msr, msre_var *var, msre + return 1; + } + ++/* MULTIPART_PART_HEADERS */ ++ ++static int var_multipart_part_headers_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, ++ apr_table_t *vartab, apr_pool_t *mptmp) ++{ ++ multipart_part **parts = NULL; ++ int i, j, count = 0; ++ ++ if (msr->mpd == NULL) return 0; ++ ++ parts = (multipart_part **)msr->mpd->parts->elts; ++ for(i = 0; i < msr->mpd->parts->nelts; i++) { ++ int match = 0; ++ ++ /* Figure out if we want to include this variable. */ ++ if (var->param == NULL) match = 1; ++ else { ++ if (var->param_data != NULL) { /* Regex. */ ++ char *my_error_msg = NULL; ++ if (!(msc_regexec((msc_regex_t *)var->param_data, parts[i]->name, ++ strlen(parts[i]->name), &my_error_msg) == PCRE_ERROR_NOMATCH)) match = 1; ++ } else { /* Simple comparison. */ ++ if (strcasecmp(parts[i]->name, var->param) == 0) match = 1; ++ } ++ } ++ ++ /* If we had a match add this argument to the collection. */ ++ if (match) { ++ for (j = 0; j < parts[i]->header_lines->nelts; j++) { ++ char *header_line = ((char **)parts[i]->header_lines->elts)[j]; ++ msre_var *rvar = apr_pmemdup(mptmp, var, sizeof(msre_var)); ++ ++ rvar->value = header_line; ++ rvar->value_len = strlen(rvar->value); ++ rvar->name = apr_psprintf(mptmp, "MULTIPART_PART_HEADERS:%s", ++ log_escape_nq(mptmp, parts[i]->name)); ++ apr_table_addn(vartab, rvar->name, (void *)rvar); ++ ++ count++; ++ } ++ } ++ } ++ ++ return count; ++} ++ + /* MODSEC_BUILD */ + + static int var_modsec_build_generate(modsec_rec *msr, msre_var *var, msre_rule *rule, +@@ -2966,6 +3012,17 @@ void msre_engine_register_default_variables(msre_engine *engine) { + PHASE_REQUEST_BODY + ); + ++ /* MULTIPART_PART_HEADERS */ ++ msre_engine_variable_register(engine, ++ "MULTIPART_PART_HEADERS", ++ VAR_LIST, ++ 0, 1, ++ var_generic_list_validate, ++ var_multipart_part_headers_generate, ++ VAR_CACHE, ++ PHASE_REQUEST_BODY ++ ); ++ + /* GEO */ + msre_engine_variable_register(engine, + "GEO", +diff --git a/tests/regression/misc/00-multipart-parser.t b/tests/regression/misc/00-multipart-parser.t +index 3c1f41b7d..e5ee4c13c 100644 +--- a/tests/regression/misc/00-multipart-parser.t ++++ b/tests/regression/misc/00-multipart-parser.t +@@ -1849,3 +1849,48 @@ + ), + }, + ++# part headers ++{ ++ type => "misc", ++ comment => "multipart parser (part headers)", ++ conf => qq( ++ SecRuleEngine On ++ SecDebugLog $ENV{DEBUG_LOG} ++ SecDebugLogLevel 9 ++ SecRequestBodyAccess On ++ SecRule MULTIPART_STRICT_ERROR "\@eq 1" "phase:2,deny,status:400,id:500168" ++ SecRule REQBODY_PROCESSOR_ERROR "\@eq 1" "phase:2,deny,status:400,id:500169" ++ SecRule MULTIPART_PART_HEADERS:image "\@rx content-type:.*jpeg" "phase:2,deny,status:403,id:500170,t:lowercase" ++ ), ++ match_log => { ++ debug => [ qr/500170.*against MULTIPART_PART_HEADERS:image.*Rule returned 1./s, 1 ], ++ }, ++ match_response => { ++ status => qr/^403$/, ++ }, ++ request => new HTTP::Request( ++ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", ++ [ ++ "Content-Type" => q(multipart/form-data; boundary=0000), ++ ], ++ normalize_raw_request_data( ++ q( ++ --0000 ++ Content-Disposition: form-data; name="username" ++ ++ Bill ++ --0000 ++ Content-Disposition: form-data; name="email" ++ ++ bill@fakesite.com ++ --0000 ++ Content-Disposition: form-data; name="image"; filename="image.jpg" ++ Content-Type: image/jpeg ++ ++ BINARYDATA ++ --0000-- ++ ), ++ ), ++ ), ++}, ++ diff --git a/mod_security.spec b/mod_security.spec index 1d38324bd7917db0245aca8d6c8b9560bf9b2e05..70f0e11a0ccbb0820f26247c106232ea657432b5 100644 --- a/mod_security.spec +++ b/mod_security.spec @@ -7,7 +7,7 @@ Name: mod_security Version: 2.9.5 -Release: 7 +Release: 9 Summary: Security module for the Apache HTTP Server License: ASL 2.0 URL: http://www.modsecurity.org/ @@ -21,6 +21,9 @@ Patch0002: modsecurity-2.9.5-Properly-cleanup-XML-parser-contexts-upon-completio Patch0003: modsecurity-2.9.5-Add-SecRequestBodyJsonDepthLimit-to-modsecurity.conf.patch Patch0004: modsecurity-2.9.5-Fix-memory-leak-that-occurs-on-JSON-parsing-error.patch Patch0005: modsecurity-2.9.5-Set-SecStatusEngine-Off-in-modsecurity.conf.patch +Patch0006: modsecurity-2.9.5-Allow-no-key-single-value-JSON-body.patch +# https://github.com/SpiderLabs/ModSecurity/commit/51a30d7b406af95c4143560d9753cf0b6d2151f5 +Patch0007: CVE-2022-48279.patch Requires: httpd httpd-mmn = %{_httpd_mmn} BuildRequires: gcc make perl-generators httpd-devel yajl yajl-devel @@ -103,6 +106,12 @@ install -m0755 mlogc/mlogc-batch-load.pl %{buildroot}%{_bindir}/mlogc-batch-load %endif %changelog +* Tue Mar 26 2024 yaoxin - 2.9.5-9 +- Fix CVE-2022-48279 + +* Mon Jan 9 2023 yaoguangzhong - 2.9.5-8 +- backport allow no-key, single-value JSON body + * Sat Jan 7 2023 yaoguangzhong - 2.9.5-7 - backport Set SecStatusEngine Off in modsecurity.conf-recommended @@ -118,7 +127,7 @@ install -m0755 mlogc/mlogc-batch-load.pl %{buildroot}%{_bindir}/mlogc-batch-load * Fri Jan 6 2023 yaoguangzhong - 2.9.5-3 - backport use uid if user name is not available -* Fri Jan 7 liyanan - 2.9.5-2 +* Fri Jan 7 2022 liyanan - 2.9.5-2 - Fix build fail with lua 5.4.3 * Tue Dec 14 2021 yaoxin - 2.9.5-1 diff --git a/modsecurity-2.9.5-Allow-no-key-single-value-JSON-body.patch b/modsecurity-2.9.5-Allow-no-key-single-value-JSON-body.patch new file mode 100644 index 0000000000000000000000000000000000000000..c746e80c6ad881cd2de9e809e910d2f250c002ee --- /dev/null +++ b/modsecurity-2.9.5-Allow-no-key-single-value-JSON-body.patch @@ -0,0 +1,89 @@ +From 630d57d7bd07696a72ac8ded7593bbcf31168a95 Mon Sep 17 00:00:00 2001 +From: yaoguangzhong +Date: Mon, 9 Jan 2023 16:00:15 +0800 +Subject: [PATCH] backport Allow no-key, single-value JSON body + +From Author: Martin Vierula +From commit 4a98032b7f827c4edd2514ce2af29222bb2ba289 +Signed-off-by: Guangzhong Yao +--- + apache2/msc_json.c | 3 +-- + apache2/msc_json.h | 2 +- + tests/regression/rule/15-json.t | 34 +++++++++++++++++++++++++++++++++ + 3 files changed, 36 insertions(+), 3 deletions(-) + +diff --git a/apache2/msc_json.c b/apache2/msc_json.c +index cbaab0e..bab3a6d 100644 +--- a/apache2/msc_json.c ++++ b/apache2/msc_json.c +@@ -27,8 +27,7 @@ int json_add_argument(modsec_rec *msr, const char *value, unsigned length) + * to reference this argument; for now we simply ignore these + */ + if (!msr->json->current_key) { +- msr_log(msr, 3, "Cannot add scalar value without an associated key"); +- return 1; ++ msr->json->current_key = ""; + } + + arg = (msc_arg *) apr_pcalloc(msr->mp, sizeof(msc_arg)); +diff --git a/apache2/msc_json.h b/apache2/msc_json.h +index 7e3d725..089dab4 100644 +--- a/apache2/msc_json.h ++++ b/apache2/msc_json.h +@@ -39,7 +39,7 @@ struct json_data { + + /* prefix is used to create data hierarchy (i.e., 'parent.child.value') */ + unsigned char *prefix; +- unsigned char *current_key; ++ const unsigned char *current_key; + long int current_depth; + int depth_limit_exceeded; + }; +diff --git a/tests/regression/rule/15-json.t b/tests/regression/rule/15-json.t +index f84355a..65f53ec 100644 +--- a/tests/regression/rule/15-json.t ++++ b/tests/regression/rule/15-json.t +@@ -224,6 +224,40 @@ + ), + ), + ), ++}, ++{ ++ type => "rule", ++ comment => "json parser - no-key single value", ++ conf => qq( ++ SecRuleEngine On ++ SecRequestBodyAccess On ++ SecDebugLog $ENV{DEBUG_LOG} ++ SecAuditEngine RelevantOnly ++ SecAuditLog "$ENV{AUDIT_LOG}" ++ SecDebugLogLevel 9 ++ SecRequestBodyJsonDepthLimit 3 ++ SecRule REQUEST_HEADERS:Content-Type "application/json" \\ ++ "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" ++ SecRule REQBODY_ERROR "!\@eq 0" "id:'200444',phase:2,log,deny,status:403,msg:'Failed to parse request body'" ++ SecRule ARGS "\@streq 25" "id:'200445',phase:2,log,deny,status:403" ++ ), ++ match_log => { ++ audit => [ qr/200445/s, 1 ], ++ }, ++ match_response => { ++ status => qr/^403$/, ++ }, ++ request => new HTTP::Request( ++ POST => "http://$ENV{SERVER_NAME}:$ENV{SERVER_PORT}/test.txt", ++ [ ++ "Content-Type" => "application/json", ++ ], ++ normalize_raw_request_data( ++ q( ++ 25 ++ ), ++ ), ++ ), + } + + +-- +2.39.0.windows.2 +