Package: release.debian.org Severity: normal Tags: bullseye User: release.debian....@packages.debian.org Usertags: pu X-Debbugs-Cc: airw...@gmail.com, christian.fol...@netnea.com
[ Reason ] modsecurity-crs has been released today [1]. It fixes a security issue, here is the announcement: -------- CVE-2022-39956 - Content-Type or Content-Transfer-Encoding MIME header fields abuse The OWASP ModSecurity Core Rule Set (CRS) is affected by a partial rule set bypass for HTTP multipart requests by submitting a payload that uses a character encoding scheme via the Content-Type or the deprecated Content-Transfer-Encoding multipart MIME header fields that will not be decoded and inspected by the web application firewall engine and the rule set. The multipart payload will therefore bypass detection. A vulnerable backend that supports these encoding schemes can potentially be exploited. The legacy CRS versions 3.0.x and 3.1.x are affected, as well as the currently supported versions 3.2.1 and 3.3.2. Integrators and users are advised to upgrade to 3.2.2 and 3.3.3 respectively. Important: The mitigation against these vulnerabilities depends on the installation of the latest ModSecurity version (v2.9.6/v3.0.8) or an updated version with backports of the security fixes in these versions. If you fail to update ModSecurity, the webserver / engine will refuse to start with the following error message: "Error creating rule: Unknown variable: MULTIPART_PART_HEADERS". You can disable / remove the rule file REQUEST-922-MULTIPART-ATTACK.conf from the release in order to allow you to run the latest CRS without a fix to CVE-2022-39956, however we advise against this workaround. ------ As you may see in [1] a newer modsecurity is needed in other to apply this fix. We, modsecurity packaging team, are preparing a patched version of both modsecurity-apache (this bug report) and libmodsecurity3 (coming up). After that we'll upload the updated modsecurity-crs. [ Impact ] No support for the fixed version of modsecurity-crs. [ Risks ] Patch is not big. It has been tested. No risks should be expected. [ Checklist ] [x] *all* changes are documented in the d/changelog|patch [x] I reviewed all changes and I approve them [x] attach debdiff against the package in (old)stable [x] the issue is verified as fixed in unstable [ Changes ] Added patch to support new required variable "MULTIPART_PART_HEADERS". Will wait for your OK before uploading. Thanks. [1] https://github.com/coreruleset/coreruleset/releases
diff -Nru modsecurity-apache-2.9.3/debian/changelog modsecurity-apache-2.9.3/debian/changelog --- modsecurity-apache-2.9.3/debian/changelog 2021-12-01 16:04:02.000000000 +0100 +++ modsecurity-apache-2.9.3/debian/changelog 2022-09-08 23:59:34.000000000 +0200 @@ -1,3 +1,9 @@ +modsecurity-apache (2.9.3-3+deb11u2) bullseye; urgency=medium + + * Added multipart_part_headers.patch + + -- Ervin Hegedus <airw...@gmail.com> Thu, 08 Sep 2022 23:59:34 +0200 + modsecurity-apache (2.9.3-3+deb11u1) bullseye-security; urgency=high * Added json_depth_limit.patch diff -Nru modsecurity-apache-2.9.3/debian/patches/multipart_part_headers.patch modsecurity-apache-2.9.3/debian/patches/multipart_part_headers.patch --- modsecurity-apache-2.9.3/debian/patches/multipart_part_headers.patch 1970-01-01 01:00:00.000000000 +0100 +++ modsecurity-apache-2.9.3/debian/patches/multipart_part_headers.patch 2022-09-08 23:59:34.000000000 +0200 @@ -0,0 +1,410 @@ +Description: This patch adds MULTIPART_PART_HEADERS variable + ModSecurity creates from now a new variable: MULTIPART_PART_HEADERS + This needs for some special CoreRuleSet rules, which has allocated CVE's. +Author: Ervin Hegedus <airw...@gmail.com> + +--- +Origin: other +Bug: not published yet +Last-Update: 2022-09-08 + +--- modsecurity-apache-2.9.3.orig/apache2/msc_multipart.c ++++ modsecurity-apache-2.9.3/apache2/msc_multipart.c +@@ -318,7 +318,14 @@ static int multipart_process_part_header + } + + 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. */ + +@@ -372,12 +379,28 @@ static int multipart_process_part_header + *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.", +@@ -431,6 +454,8 @@ static int multipart_process_part_data(m + 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') ) +@@ -673,10 +698,14 @@ static int multipart_process_boundary(mo + 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; +@@ -976,6 +1005,19 @@ int multipart_complete(modsec_rec *msr, + && (*(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; +@@ -1068,54 +1110,62 @@ int multipart_process_chunk(modsec_rec * + 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; +- +- /* 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; +- } ++ 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; + +- /* 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; ++ /* 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; ++ } + } + +- if (multipart_process_boundary(msr, (is_final ? 1 : 0), error_msg) < 0) { ++ /* 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 (multipart_process_boundary(msr, (is_final ? 1 : 0), error_msg) < 0) { ++ msr->mpd->flag_error = 1; ++ 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; + } +- +- 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; +@@ -1214,6 +1264,21 @@ int multipart_process_chunk(modsec_rec * + 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)) { +--- modsecurity-apache-2.9.3.orig/apache2/msc_multipart.h ++++ modsecurity-apache-2.9.3/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 +--- modsecurity-apache-2.9.3.orig/apache2/re_variables.c ++++ modsecurity-apache-2.9.3/apache2/re_variables.c +@@ -1394,6 +1394,52 @@ static int var_files_combined_size_gener + 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, +@@ -2965,6 +3011,17 @@ void msre_engine_register_default_variab + VAR_CACHE, + 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, +--- modsecurity-apache-2.9.3.orig/modsecurity.conf-recommended ++++ modsecurity-apache-2.9.3/modsecurity.conf-recommended +@@ -19,14 +19,14 @@ SecRequestBodyAccess On + # Enable XML request body parser. + # Initiate XML Processor in case of xml content-type + # +-SecRule REQUEST_HEADERS:Content-Type "(?:application(?:/soap\+|/)|text/)xml" \ ++SecRule REQUEST_HEADERS:Content-Type "^(?:application(?:/soap\+|/)|text/)xml" \ + "id:'200000',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=XML" + + # Enable JSON request body parser. + # Initiate JSON Processor in case of JSON content-type; change accordingly + # if your application does not use 'application/json' + # +-SecRule REQUEST_HEADERS:Content-Type "application/json" \ ++SecRule REQUEST_HEADERS:Content-Type "^application/json" \ + "id:'200001',phase:1,t:none,t:lowercase,pass,nolog,ctl:requestBodyProcessor=JSON" + + # Maximum request body size we will accept for buffering. If you support +--- modsecurity-apache-2.9.3.orig/tests/regression/misc/00-multipart-parser.t ++++ modsecurity-apache-2.9.3/tests/regression/misc/00-multipart-parser.t +@@ -1811,3 +1811,47 @@ + ), + }, + ++# 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" ++ ++ b...@fakesite.com ++ --0000 ++ Content-Disposition: form-data; name="image"; filename="image.jpg" ++ Content-Type: image/jpeg ++ ++ BINARYDATA ++ --0000-- ++ ), ++ ), ++ ), ++}, diff -Nru modsecurity-apache-2.9.3/debian/patches/series modsecurity-apache-2.9.3/debian/patches/series --- modsecurity-apache-2.9.3/debian/patches/series 2021-12-01 16:04:02.000000000 +0100 +++ modsecurity-apache-2.9.3/debian/patches/series 2022-09-08 23:59:34.000000000 +0200 @@ -2,3 +2,4 @@ improve_defaults.patch 970833_fix.patch json_depth_limit.patch +multipart_part_headers.patch