We would like to share our take on several advisories, which were published on September 29, 2026. There were 8 issues in libmodsecurity3; 6 of them are present in mod_security2 as well.
Security Update Digest: Overview of Recent Advisory Fixes
Maintaining code quality, stability, and user security is our top priority. Over the past three months, our maintainers have been actively auditing the codebase, reviewing community-submitted security reports, and resolving potential edge-case vulnerabilities. Please note that there are some advisories here which don’t have CVEs. The reason is that we requested CVE through Github, but they haven’t been assigned yet. In the future we will only include advisories and optionally CVE IDs.
Security Advisories Overview
GHSA-cxqf-vgrr-xxrv: HTML decoder missing entities, leading to evasion
- Severity: MODERATE (CVSS: 5.3)
- CVE: CVE-2026-61812
- Affected Versions:
<= 3.0.16, <= 2.9.14 - Patched Versions:
3.0.17, 2.9.15 - Timeline: Reported on 2024-08-02 | Published on 2026-09-27
- Reported by: Marc Stern
- Fixed by / Contributors: Felipe Zipitria
- GitHub Advisory: GHSA-cxqf-vgrr-xxrv
Summary: Two named entities were originally reported missing from the HTML entity decoder (' and :). Triage established that the decoder recognized only five named entities in total, and that the hand-maintained list had drifted from the HTML specification. The fix replaces that list with a table generated from the WHATWG named character reference list.
Impact: Rules using t:htmlEntityDecode to normalize input before matching could be evaded by encoding ASCII payload bytes as HTML named entities the decoder didn’t recognize. The decoder knew only five names (quot, amp, lt, gt, nbsp); every other named entity was copied through unchanged rather than decoded. For example, "javascript:execute_my_code();" was not detected by a rule checking for "javascript:" with t:htmlEntityDecode.
libmodsecurity3 was additionally affected by a related defect: it compared entity names with strncasecmp() using only the candidate name’s own length, so an unrecognized name sharing a recognized prefix (e.g. <est;) decoded to the prefix’s character and silently dropped the remaining bytes. mod_security2 duplicated the token and compared it in full, and was not affected by that second issue.
Notes: Exploitation requires the & to reach the transformation intact. In an ordinary urlencoded query string it does not — the argument parser splits on &, so ?q=javascript:... becomes two arguments and no entity is ever seen. The payload must therefore arrive percent-encoded (%26), or in a JSON body, cookie, header or path. Decoding is also single-pass, as in a browser: &colon; decodes to :, not to :, so double-encoded input still requires t:htmlEntityDecode to be applied twice.
This issue was originally reported in 2024 and re-triaged in 2026, which is why its reporting date predates the others in this digest.
Upgrade impact: the decoder now recognizes considerably more entities, so values containing literal text such as ; or $ are decoded where previously they were not, and rules may match input they did not match before — rule set maintainers should re-test. 
 and 	 decode to real control characters, which will appear in transformed values and in logdata and audit log output.
Resolution: Fixed in mod_security2 2.9.15 and libmodsecurity3 3.0.17. Both engines’ t:htmlEntityDecode now use a length-aware, case-insensitive lookup table of 44 entries, replacing the previous if/else cascade. The table is generated by tools/gen-html-entities.py from the WHATWG named character reference list (https://html.spec.whatwg.org/entities.json) and contains every entity that expands to a single code point in U+0000–U+007F, plus U+00A0 (NBSP) and U+00AD (SOFT HYPHEN). Both engines carry an identical table. A name decodes only when the full extracted token matches an entry on both name and length, which also closes the prefix-collision defect in libmodsecurity3.
Entities expanding outside that set — accented characters, typographic symbols, multi-code-point expansions — remain intentionally unsupported: the transformation exists to reconstruct ASCII payload bytes, not to perform full HTML decoding. Note that some names that look like ASCII punctuation are not ASCII in the specification and are therefore not decoded, for example ‐ (U+2010) and ˜ (U+02DC).
Workaround: None. Upgrade to a patched release.
GHSA-2vqc-36qp-ccmw: Weak libcurl TLS hostname verification setting when fetching over HTTPS
- Severity: LOW (CVSS: 3.7)
- CVE: CVE-2026-61813
- Affected Versions:
<= 3.0.16, <= 2.9.14 - Patched Versions:
3.0.17, 2.9.15 - Timeline: Reported on 2026-06-19 | Published on 2026-09-27
- Reported by: amitu314
- Fixed by / Contributors: amitu314, Ervin Hegedüs
- GitHub Advisory: GHSA-2vqc-36qp-ccmw
Summary: Both engines set CURLOPT_SSL_VERIFYHOST to 1 when fetching content over HTTPS. 1 is not libcurl’s strict hostname verification mode; the correct value is 2.
Impact: A non-strict hostname verification setting can weaken TLS peer identity validation. In practice the exposure depends on the libcurl version the engine was built against: libcurl rejects the value 1 and leaves its default of 2 in place from 7.28.1 to 7.65.3, and treats 1 exactly like 2 from 7.66.0 onwards. Hostname verification was therefore only actually weakened for builds linked against libcurl 7.28.0 or older, released before November 2012. CURLOPT_SSL_VERIFYPEER was set to 1 throughout, so certificate chain validation was never affected, and a minimum of TLS 1.2 was already enforced.
Notes: The affected code only exists in builds compiled with curl support (MSC_WITH_CURL in libmodsecurity3, WITH_CURL in mod_security2), and it is only reached when the configuration fetches something over HTTPS: SecRemoteRules in both engines, and additionally a URL-based IP list (IpTree::addFromUrl) in libmodsecurity3. It is not reachable from request traffic.
Resolution: Fixed in mod_security2 2.9.15 and libmodsecurity3 3.0.17 by setting CURLOPT_SSL_VERIFYHOST to 2L, libcurl’s strict hostname verification mode — in HttpsClient::download (src/utils/https_client.cc) in libmodsecurity3 and in msc_remote_download_content() (apache2/msc_remote_rules.c) in mod_security2. CURLOPT_SSL_VERIFYPEER remains set to 1.
Workaround: None. Immediate upgrade is recommended.
GHSA-jx3r-phvx-2jmj: XML request body processor dereferences an uninitialized parser context pointer
- Severity: HIGH (CVSS: 7.5)
- CVE: CVE-2026-73857
- Affected Versions:
>= 3.0.15, <= 3.0.16 - Patched Versions:
3.0.17 - Timeline: Reported on 2026-07-03 | Published on 2026-09-27
- Reported by: Tobias Klein (www.trapkit.de)
- Fixed by / Contributors: Tobias Klein (www.trapkit.de), Ervin Hegedüs
- GitHub Advisory: GHSA-jx3r-phvx-2jmj
Summary: A remote, unauthenticated attacker can, with a single crafted HTTP request, make a web server worker process running ModSecurity dereference an uninitialized pointer (XMLNodes::parsing_ctx_arg) in the XML-into-ARGS SAX end-element callback, resulting in a write of a constant value to an attacker-controlled address (a restricted write-what-where primitive)
Impact: Impact ranges from denial of service to memory disclosure and execution-flow redirection (possibly leading to remote code execution).
Notes: The defect is reachable only when SecParseXmlIntoArgs is set to On or OnlyArgs; it is Off by default. libmodsecurity3 only: mod_security2 allocates its XML state with apr_pcalloc(), which zeroes the memory, so the pointer was already NULL there. mod_security2 nevertheless received the same guard and an explicit initialization as defence in depth.
Resolution: Fixed in libmodsecurity3 3.0.17. XMLNodes::parsing_ctx_arg is now initialized to NULL in the constructor; XML::processChunk() assigns the parser context on the first-chunk path as well, where previously it was only set once a second chunk arrived; and the SAX end-element callback checks the pointer before calling xmlStopParser(). The guard alone stops the dereference, while the assignment is what makes SecArgumentsLimit actually terminate parsing for a single-chunk request body, as intended. mod_security2 received the equivalent guard and initialization.
Workaround: Don’t use SecParseXmlIntoArgs with On or OnlyArgs directive. Immediate upgrade is recommended.
GHSA-vmg8-j66p-vgvw: Response body inspection bypass via non-canonical Content-Type casing
- Severity: HIGH (CVSS: 8.6)
- CVE: CVE-2026-73856
- Affected Versions:
<= 3.0.16 - Patched Versions:
3.0.17 - Timeline: Reported on 2026-07-07 | Published on 2026-09-27
- Reported by: zuesdevil
- Fixed by / Contributors: Ervin Hegedüs
- GitHub Advisory: GHSA-vmg8-j66p-vgvw
Summary: The engine compared the response Content-Type against the MIME type list configured with SecResponseBodyMimeType using an exact, case-sensitive match. A backend returning Text/Html instead of text/html therefore did not match the configured list, and the response body was not inspected: no RESPONSE_BODY rules were evaluated and the body was forwarded to the client, with only a debug-log line recording that inspection had been skipped.
Impact:
- Leaks sensitive data that would otherwise be blocked by the RESPONSE_BODY rule
- Retrieves complete SQL error messages, stack traces, and debug information
Notes: libmodsecurity3 only. mod_security2 lowercases the header before the lookup and uses an APR table, whose key comparison is case-insensitive, so it never matched case-sensitively. The defect requires SecResponseBodyAccess On and a configured SecResponseBodyMimeType — the library ships no built-in MIME list, the familiar text/plain text/html text/xml comes from modsecurity.conf-recommended. With the directive absent entirely, no Content-Type filtering is applied and every response body is inspected. Note for upgrades: the configured MIME type list itself is still matched as written, so a SecResponseBodyMimeType value entered with non-lowercase characters (for example Text/Html) no longer matches and its responses are no longer inspected. Enter MIME types in lowercase.
Resolution: Fixed in libmodsecurity3 3.0.17 by lowercasing the response Content-Type before it is looked up in the configured MIME type list, in both Transaction::processResponseBody() and Transaction::appendResponseBody() (src/transaction.cc), with a regression case covering a mixed-case Content-Type.
Workaround: None. Immediate upgrade is recommended.
GHSA-5m93-4h75-3p2w: @rxGlobal PCRE2 error handling: match-limit fail-open and invalid-pattern crash
- Severity: MODERATE (CVSS: 5.8)
- CVE: No CVE was allocated for this vulnerability.
- Affected Versions:
>= 3.0.5, <= 3.0.16 - Patched Versions:
3.0.17 - Timeline: Reported on 2026-07-24 | Published on 2026-09-27
- Reported by: AnnoyingTechnology
- Fixed by / Contributors: Ervin Hegedüs
- GitHub Advisory: GHSA-5m93-4h75-3p2w
Summary: Two defects in the PCRE2 code path of @rxGlobal, both present since the operator was introduced in 3.0.5. First, Regex::searchGlobal() converted PCRE2 match-limit errors into an ordinary no-match, so an unauthenticated remote client could make a request pass a compensating rule by supplying input that exhausts SecPcreMatchLimit. Second, @rxGlobal was missing the invalid-pattern guard that @rx already had, so a macro-expanded pattern that fails to compile left the operator dereferencing a null PCRE2 code pointer.
Impact: The first defect is a remotely triggerable security-control fail-open and an observability failure. The affected population is ModSecurity v3 deployed with PCRE2 where:
- a ruleset applies
@rxGlobalto attacker-controlled input; - that regular expression can exhaust the configured PCRE match limit;
- and the ruleset relies on
MSC_PCRE_LIMITS_EXCEEDEDor its TX compatibility indicator to block or otherwise compensate for incomplete inspection.
The second defect crashes the worker process. It applies where a ruleset builds an @rxGlobal pattern from a macro whose value can be influenced by the request, for example @rxGlobal %{ARGS:pattern}; a single request carrying a pattern that does not compile is enough.
Notes: A CVE identifier was requested through GitHub on 2026-07-25 and had not been assigned at the time of publication. Upgrade impact: regular expressions are now validated when the rule is loaded, so a configuration containing a pattern that does not compile no longer starts. Such a rule previously loaded and simply never matched, so the problem can be latent in an existing ruleset; the error message names the offending pattern and the line it is on.
Resolution: Fixed in libmodsecurity3 3.0.17. Regex::searchGlobal() now converts the PCRE2 return code into a RegexResult and propagates errors to the caller, as the PCRE1 code path already did. The MSC_PCRE_ERROR, MSC_PCRE_LIMITS_EXCEEDED and TX.MSC_PCRE_LIMITS_EXCEEDED handling in RxGlobal::evaluate() already existed, but was unreachable on PCRE2 builds because the match loop never reported a failure. RxGlobal::evaluate() also gained the invalid-pattern guard that Rx::evaluate() already had, and both operators now validate their pattern when the rule is loaded, rejecting one that does not compile instead of loading it and silently never matching. MSC_PCRE_ERROR is now set on a compilation error as well.
Workaround: The fail-open has no configuration mitigation short of not using @rxGlobal at all. The crash is additionally avoided by not building an @rxGlobal pattern from a macro whose value can come from the request. Immediate upgrade is recommended.
GHSA-qrch-pjfr-9g47: t:removeComments mishandles the character after a comment terminator, bypassing rules
- Severity: MODERATE (CVSS: 5.8)
- CVE: No CVE was allocated for this vulnerability.
- Affected Versions:
<= 3.0.16, <= 2.9.14 - Patched Versions:
3.0.17, 2.9.15 - Timeline: Reported on 2026-07-25 | Published on 2026-09-27
- Reported by: HEXER365
- Fixed by / Contributors: Ervin Hegedüs
- GitHub Advisory: GHSA-qrch-pjfr-9g47
Summary: t:removeComments copied the character following a comment terminator straight to the output without re-examining it. When that character began a second comment, the second comment survived: UNION/**//**/SELECT came out as UNION/**/SELECT, letting inline-comment obfuscation defeat detection rules that rely on comment stripping. The same copy had two further effects. A value ending with a comment terminator gained a trailing NUL byte, so UNION SELECT/**/ became UNION SELECT\x00; a single comment at the end of the input is enough to trigger this. And two adjacent HTML comments truncated the value, so <!--a--><!--b-->x was inspected as <! while the application still received the original bytes.
Impact:
- WAF rule bypass (CWE-670): inline-comment obfuscation such as
/**/UNION/**/SELECT/**/orUNION/**//**/SELECTsurvivest:removeCommentsand reaches rule matching unmodified. - The trailing NUL byte is enough on its own to defeat a rule anchored at the end of the input, and it needs only one comment, at the end of the value — not an adjacent pair.
- With adjacent HTML comments the transformed value is truncated, so the payload following them is never inspected at all.
- No memory-safety impact; this is a logic and rule-bypass issue.
Notes: A CVE identifier was requested through GitHub on 2026-07-27 and had not been assigned at the time of publication.
Resolution: Fixed in libmodsecurity3 3.0.17 and mod_security2 2.9.15. Both engines dropped the three lines that copied and skipped the character following */ or -->, so that character is now re-examined by the transformation loop. This strips adjacent comments, removes the spurious trailing NUL byte, and stops the truncation that occurred when an HTML comment terminator was followed by another comment.
Workaround: Don’t use t:removeComments transformation. Immediate upgrade is recommended.
GHSA-4j47-8qcr-jf59: t:base64DecodeExt does not decode - and _, bypassing rules on URL-safe encoded payloads
- Severity: MODERATE (CVSS: 5.8)
- CVE: No CVE was allocated for this vulnerability.
- Affected Versions:
<= 3.0.16, <= 2.9.14 - Patched Versions:
3.0.17, 2.9.15 - Timeline: Reported on 2026-07-25 | Published on 2026-09-27
- Reported by: Felipe Zipitria
- Fixed by / Contributors: Ervin Hegedüs
- GitHub Advisory: GHSA-4j47-8qcr-jf59
Summary: t:base64DecodeExt treated - and _ as invalid characters and skipped them, instead of decoding them as the URL and filename safe substitutes for + and / defined in RFC 4648 section 5. A value containing either character therefore decoded to garbage or to a truncated result, and a rule matching against the decoded content did not see the payload. JSON Web Tokens use that alphabet, so a rule that decodes a JWT segment in order to inspect it — to detect "alg":"none", for example — silently failed whenever that segment contained - or _.
Impact: Any rule that applies t:base64DecodeExt to attacker-controlled input and matches against the decoded content can be bypassed by encoding the payload with - and _ in place of + and /. For JWTs this takes no effort at all, since that is their standard encoding. The transformation reports success either way, so nothing in the logs indicates that the value was not decoded correctly.
Notes: A CVE identifier was requested through GitHub on 2026-08-03 and had not been assigned at the time of publication.
Input that is not valid base64 is deliberately left undecoded. A group carrying a single data character before == padding, as in dzBzV==, is malformed, and t:base64DecodeExt yields an empty result for the whole value rather than guessing at a reconstruction — no base64 decoder recovers the original bytes from such input. That behaviour is unchanged and is covered by the regression tests.
Resolution: Fixed in libmodsecurity3 3.0.17 and mod_security2 2.9.15 by mapping - to 62 and _ to 63 in the base64 reverse lookup table that t:base64DecodeExt uses — the values already assigned to + and / — in src/utils/base64.cc and apache2/msc_util.c respectively. Note that t:base64Decode is a strict decoder: it continues to reject such input and leaves the value unchanged, so only t:base64DecodeExt accepts both alphabets.
Workaround: There is no configuration-level mitigation; without the fix the decoded value is simply wrong. A rule can additionally match the raw, undecoded value, but that is not equivalent to inspecting the decoded content. Immediate upgrade is recommended.
GHSA-5pww-8rfg-9crf: RFC 2231 filename* parameter bypasses multipart filename rules
- Severity: HIGH (CVSS: 8.6)
- CVE: No CVE was allocated for this vulnerability.
- Affected Versions:
<= 3.0.16, <= 2.9.14 - Patched Versions:
3.0.17, 2.9.15 - Timeline: Reported on 2026-07-26 | Published on 2026-09-27
- Reported by: 0xkalawy, ZeyadZonkorany, Mohamed Gouda
- Fixed by / Contributors: Hiroaki Nakamura, Felipe Zipitria, Ervin Hegedüs
- GitHub Advisory: GHSA-5pww-8rfg-9crf
Summary: Content-Disposition headers in a multipart body may carry the file name twice: as a plain filename parameter and as an RFC 2231 encoded filename* parameter that also states a charset and an optional language. RFC-compliant parsers prefer filename* when both are present. ModSecurity’s multipart parser did not implement RFC 2231: it recognised filename only, and a part that carried filename* alone populated no file name at all, logging a warning instead. Rules that inspect uploaded file names — through MULTIPART_FILENAME, FILES, or a regex over the raw header, as several OWASP CRS rules do — therefore saw either nothing or the decoy value from filename, while the application acted on the filename* value.
Impact: An attacker can put a harmless name in filename and the real one in filename*, or omit filename entirely. A rule matching on the uploaded file name does not fire, while a backend written in Go, Python, Node.js or Java — whose standard multipart parsers implement RFC 2231 — receives and acts on the filename* value. The bypass needs a single request and no unusual encoding beyond the parameter that the specification itself defines.
Notes: A CVE identifier was requested through GitHub on 2026-07-27 and had not been assigned at the time of publication.
Behaviour changes to review before upgrading.
- A part carrying only
filename*now populatesMULTIPART_FILENAME. Previously it populated nothing and logged “no filename= but filename*”, so rules that quietly never matched such a part will start matching. - When both parameters are present,
MULTIPART_FILENAMEnow holds thefilename*value, not thefilenamevalue. - In mod_security2 the
MULTIPART_*variables were scalars, which meant that with several parts only the last one’s value survived and the earlier parts were invisible to rules. They are now collections and can be addressed by key, for exampleMULTIPART_NAME:file. As a result a rule can now match more than once per request, andMATCHED_VARSandMATCHED_VARS_NAMEScan hold several entries where they previously held at most one. Rules that count matches, or that rely on a negated operator against these variables, should be re-tested. - The value of
filename*is URL decoded, but+is left as+rather than being turned into a space, because the RFC 2231 encoding is not form encoding.
Resolution: Fixed in libmodsecurity3 3.0.17 and mod_security2 2.9.15. The multipart parser now implements RFC 2231: it decodes filename*, gives it precedence over filename as RFC-compliant parsers do, and records the charset and language it declares. Two new collections expose those, MULTIPART_FILENAME_CHARSET and MULTIPART_FILENAME_LANGUAGE, so a rule can require a sane charset:
SecRule MULTIPART_FILENAME_CHARSET:name "!@within utf8 ascii" \
"id:1,phase:2,t:none,t:lowercase,drop"
A third variable, MULTIPART_DUPLICATE_PART_HEADER, is set when a part header — including name, filename or filename* — appears more than once, and is included in the multipart strict-check rule shipped in modsecurity.conf-recommended as DH. In mod_security2 the MULTIPART_* variables also became collections; see the notes above.
Workaround: None that is equivalent. Dropping requests whose Content-Disposition contains filename* at all will stop the bypass but also rejects legitimate uploads that use it. Immediate upgrade is recommended.
Conclusion & Acknowledgments
We extend our sincere gratitude to all security researchers, contributors, and community members who responsibly disclosed these issues and worked closely with us to verify, fix, and release patch builds. Responsible disclosure is a fundamental pillar of open-source security, and we deeply appreciate your ongoing collaboration.