Summary
Coraza extracts a multipart file upload's filename using Go's standard-library mime.ParseMediaType, which only decodes the RFC 5987/6266 extended filename* Content-Disposition parameter when its declared charset is exactly us-ascii or utf-8. Any other charset — including iso-8859-1, which RFC 5987 explicitly permits — is silently dropped by the stdlib, with no error and no fallback signal. This lets an attacker present one filename to Coraza and a different one to the backend application in the same request.
Affected variables
FILES (documented as "the original filenames as submitted by the client in the multipart upload"), FILES_SIZES, and the file-vs-field routing decision that determines whether a part is tracked as a file at all.
MULTIPART_FILENAME is not affected. It is declared in Coraza but is not populated by any code path in the affected versions.
Details
internal/bodyprocessors/multipart.go's originFileName calls mime.ParseMediaType on the raw Content-Disposition header and reads dispositionParams["filename"], which then populates FILES/FILES_SIZES for parts recognized as file uploads. Per RFC 7578 §4.2 and RFC 6266, when both filename and filename* are present, filename* is authoritative. Go's mime.ParseMediaType implements this precedence internally (decode2231Enc in mediatype.go), but only for filename* values whose charset token is us-ascii or utf-8:
// mime/mediatype.go
if charset != "us-ascii" && charset != "utf-8" {
// TODO: unsupported encoding
return "", false
}
When decode2231Enc returns false, the stdlib silently leaves whatever was parsed for the plain filename parameter untouched (or leaves filename absent entirely if only filename* was supplied) — there is no error, no partial-parse indicator, nothing a caller can detect. The stdlib also discards the RFC 5987 language component of filename* entirely; it is not recoverable from the parsed params map at all.
Verified behavior (prior to the fix)
Content-Disposition fields |
Effective filename (FILES) |
Recognized as file upload? |
filename="safe.jpg"; filename*=UTF-8''shell.php |
shell.php (correct) |
yes |
filename="safe.jpg"; filename*=iso-8859-1''shell.php |
safe.jpg (decoy wins) |
yes, but with the wrong name |
filename*=iso-8859-1''shell.php (no plain filename) |
(none) |
no — processed as an ordinary form field into ARGS_POST instead |
In the second row, any rule inspecting FILES for a dangerous extension only ever sees safe.jpg, while a backend that follows RFC 7578 precedence and accepts iso-8859-1 (an RFC-5987-legal, non-exotic charset) uses shell.php as the filename. In the third row, the upload skips file-specific rule scope and size-limit tracking (FILES_COMBINED_SIZE) entirely.
Impact
A rule set that blocks file uploads by extension or name via FILES (or scopes rules to FILES/FILES_NAMES) can be bypassed by supplying the real filename via filename* under any charset other than utf-8/us-ascii — iso-8859-1 is sufficient and is a standards-compliant choice, not an obscure or malformed one — optionally alongside an innocuous decoy filename. A part carrying only filename* additionally escapes file-vs-field routing, so FILES, FILES_NAMES and FILES_COMBINED_SIZE never see it.
The bypass is deterministic against Coraza and requires no authentication or user interaction. Its effect is bounded to filename-derived inspection: request body content, ARGS and headers are still inspected, and a part carrying only filename* is routed into ARGS_POST. Coraza itself neither stores nor serves the uploaded file, so whether an evaded rule was preventing a change of state depends on the backend's own filename* precedence handling and upload handling. This is scored as a scope-changing bypass with low integrity impact (S:C, I:L) rather than a direct compromise.
Proof of Concept
Content-Disposition: form-data; name="upload"; filename="safe.jpg"; filename*=iso-8859-1''shell.php
Processed through internal/bodyprocessors/multipart.go prior to the fix, FILES for this part contained safe.jpg. A SecRule FILES "\.php$" (or similar extension-blocklist rule) did not fire, while a backend honoring RFC 5987/7578 filename* precedence uses shell.php as the filename.
Resolution
Fixed by locating and decoding filename* independently of mime.ParseMediaType's charset restriction (a quoted-string-aware scanner over the raw header). The fix also populates MULTIPART_FILENAME/MULTIPART_NAME, which were previously declared but unpopulated.
Both readings are exposed, not just filename*. An earlier version of this fix made filename* unconditionally authoritative over the plain filename, per RFC 7578 §4.2 — this closed the PoC above but relocated the same bypass: swap which field carries the real name (filename="shell.php"; filename*=iso-8859-1''safe.jpg) and a backend that resolves the plain filename instead is missed, since the discarded reading never reached FILES at all. Fixed in 89399e67: when a well-formed filename* picks a different value than the plain filename, both are added to FILES/FILES_SIZES/MULTIPART_FILENAME for the same part (one temp file, one byte count, two names). A SecRule FILES "\.php$" now catches the payload regardless of which field carries the real name.
Coraza does not select a "safe" charset allowlist on the operator's behalf. Instead, filename*'s declared charset and language are exposed as their own rule-matchable collections, MULTIPART_FILENAME_CHARSET and MULTIPART_FILENAME_LANGUAGE, alongside MULTIPART_FILENAME, so operators can enforce their own allowlist. Design credit: @airween.
For Content-Disposition: form-data; name="upload"; filename="safe.jpg"; filename*=UTF-8''shell.php:
MULTIPART_FILENAME:upload = [shell.php, safe.jpg] (filename* first, plain filename kept alongside)
MULTIPART_FILENAME_CHARSET:upload = UTF-8
MULTIPART_FILENAME_LANGUAGE:upload = "" (empty string; language is optional)
Rule writers can enforce a charset allowlist directly. Use an anchored regex
rather than @within:
SecRule MULTIPART_FILENAME_CHARSET:upfile "!@rx ^(?:utf-8|iso-8859-1|us-ascii)$" \
"id:199,phase:2,t:none,t:lowercase,deny"
!@within utf-8,iso-8859-1,us-ascii looks equivalent and is not. @within
treats its parameter as the haystack and the variable as the needle, so an
empty value is trivially contained in it and the negation never fires. A
filename* that declares no charset at all — filename*=''shell.php, which
RFC 5987's grammar does not permit but which Coraza accepts and exposes rather
than rejecting — therefore passes an @within allowlist untouched. Measured:
MULTIPART_FILENAME_CHARSET |
!@within utf-8,... |
!@rx ^(?:utf-8|...)$ |
UTF-8 |
quiet |
quiet |
shift_jis |
fires |
fires |
(empty, from filename*=''...) |
quiet |
fires |
Both spellings stay quiet on a part with no filename* at all, because the
collections are then absent rather than empty and the rule never evaluates.
That is what makes an allowlist rule safe to apply to all traffic rather than
only to known upload fields.
Discrepancies are no longer dropped silently
The same silent-fallback pattern this advisory describes existed on two neighbouring paths: a part whose Content-Disposition was present but unparseable, and a part carrying a filename* that did not match the charset'[language]'value shape at all. Both were reduced to a part with no filename — routed into ARGS_POST, outside FILES-scoped rule coverage, with nothing to signal that a filename had been discarded.
Both now raise MULTIPART_STRICT_ERROR (rule 200003 in CRS). A part with no Content-Disposition at all is not flagged: it simply carries no filename. mime.ParseMediaType accepts every real-world filename shape tested — raw UTF-8 and emoji, spaces, Windows backslashes, a bare %, unquoted values — so this signal does not reach legitimate uploads.
New variable: MULTIPART_DUPLICATE_PART_HEADER
Set to 1 when a part repeats a header (two Content-Disposition headers) or repeats a parameter inside its Content-Disposition (two filename parameters). A duplicate is precisely where backends disagree about which value wins, so the duplicate itself is the signal; it also contributes to MULTIPART_STRICT_ERROR.
SecRule MULTIPART_STRICT_ERROR "@eq 1" "id:201,phase:2,deny,t:none,chain"
SecRule MULTIPART_DUPLICATE_PART_HEADER "@eq 1"
ModSecurity adds the same variable in its fix for GHSA-5pww-8rfg-9crf and lists it in the recommended audit-log format as DH. Coraza implements it under the same name so that a rule set referencing it parses on both engines.
Note on percent-decoding
Coraza percent-decodes the filename* value, so MULTIPART_FILENAME and FILES hold the name the application will actually resolve. This matters for rules that match on file extension: CRS 933110 and 944140 target FILES with t:none,t:lowercase,t:removeWhitespace and perform no URL decoding, so an encoded separator such as filename*=UTF-8''shell%2Ephp would evade them if the raw value were stored instead. ModSecurity's fix stored the raw, still-encoded value when this was first written; after the difference was raised on its advisory it now percent-decodes as well, so the two engines agree on this point.
Three further discrepancies found in post-fix review
Reviewing the fix (23446b5e) against test coverage turned up three more cases where Coraza's parsed value could disagree with what a backend resolves, closed in f30df398:
- An unresolvable
% escape in filename* (filename*=UTF-8''safe.jpg%ZZ) is still kept as-is rather than substituting a decoy plain filename, but now also raises MULTIPART_STRICT_ERROR. Without this, the discrepancy was silent whenever a backend decoded the same escape differently or rejected it outright.
- A quoted
filename* value (filename*="UTF-8''shell.php") is unwrapped before parsing. RFC 5987 does not permit filename* to be a quoted-string, but a general Content-Disposition parser such as Go's mime.ParseMediaType accepts a quoted value for any parameter. Without unwrapping, the literal quote characters ended up in MULTIPART_FILENAME/MULTIPART_FILENAME_CHARSET, so an anchored rule (e.g. \.php$) did not match a filename the backend resolved cleanly.
MULTIPART_FILENAME, MULTIPART_FILENAME_CHARSET, MULTIPART_FILENAME_LANGUAGE and MULTIPART_NAME are multi-valued collections: two parts sharing one name (e.g. a multi-file upload field) each keep their own filename instead of the later part overwriting the earlier one. ModSecurity's fix for GHSA-5pww-8rfg-9crf independently identifies and fixes the same class of bug, describing it as "a second, distinct bypass," by keeping the equivalent variables multi-valued.
The precedence decision itself was found to relocate the bypass
Reviewing filename*-as-authoritative, @jptosso demonstrated (with a repro against Go's own mime/multipart as the reference backend) that this precedence choice doesn't close the two-sided decoy: it only decides which of the two payload shapes above is caught, not both. Checked against ModSecurity v3's actual fix for GHSA-5pww-8rfg-9crf: it makes the identical unconditional-precedence choice (single value, filename* wins, plain filename discarded), with no record of the two-sided case being considered there either — this is not a case of Coraza deviating from a more careful upstream fix.
Fixed in 89399e67 by keeping both readings rather than picking one (see Resolution above). Test coverage for the original PoC direction only ever exercised the filename="safe.jpg"; filename*=...''shell.php order; the swapped order (filename="shell.php"; filename*=...''safe.jpg) is now a dedicated regression case, since that gap in coverage is exactly why the one-sided version shipped in the first place.
Patched in 3.8.1
The 3.8.0 fix was incomplete. 3.8.1 completes it: an RFC 2231 numbered continuation (filename*0=, filename*0*=, ...) next to a bare filename* parameter now sets MULTIPART_STRICT_ERROR, and the Content-Disposition duplicate-parameter check added in 3.8.0 is now linear instead of quadratic in the number of parameters. Upgrade to 3.8.1; 3.8.0 is listed as affected.
Severity (revised 2026-10-02)
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N (4.0, Medium).
Attack Complexity is High because the bypass depends on a specific backend behaviour outside the attacker's control. Go's mime.ParseMediaType and PHP resolve the same filename Coraza did, so they are not affected. The discrepancy exists only for backends that decode a non-UTF-8 filename* or treat a filename*-only part as a file, such as Werkzeug/Flask and busboy-based Node.js backends (Express with multer, Fastify). The continuation case fixed in 3.8.1 is resolved only by Werkzeug. The previous vector scored Attack Complexity Low (5.8).
Impact metrics follow the convention used across Coraza's WAF-bypass advisories: the vulnerable component is Coraza, but the impact lands on the protected application, so Scope is Changed. The bypass hides a payload from inspection; the application still has to be vulnerable to it, so Integrity is Low and Confidentiality is not scored separately.
AI involvement in this section: Claude Opus 5.5 (Anthropic), via Claude Code, re-derived the CVSS vector from the project's triage guidance (AGENTS.md, "CVSS preconditions get verified, not copied from the report") and drafted this text. A human maintainer (fzipi) chose the S:C/I:L impact convention and directed this update.
Summary
Coraza extracts a multipart file upload's filename using Go's standard-library
mime.ParseMediaType, which only decodes the RFC 5987/6266 extendedfilename*Content-Dispositionparameter when its declared charset is exactlyus-asciiorutf-8. Any other charset — includingiso-8859-1, which RFC 5987 explicitly permits — is silently dropped by the stdlib, with no error and no fallback signal. This lets an attacker present one filename to Coraza and a different one to the backend application in the same request.Affected variables
FILES(documented as "the original filenames as submitted by the client in the multipart upload"),FILES_SIZES, and the file-vs-field routing decision that determines whether a part is tracked as a file at all.MULTIPART_FILENAMEis not affected. It is declared in Coraza but is not populated by any code path in the affected versions.Details
internal/bodyprocessors/multipart.go'soriginFileNamecallsmime.ParseMediaTypeon the rawContent-Dispositionheader and readsdispositionParams["filename"], which then populatesFILES/FILES_SIZESfor parts recognized as file uploads. Per RFC 7578 §4.2 and RFC 6266, when bothfilenameandfilename*are present,filename*is authoritative. Go'smime.ParseMediaTypeimplements this precedence internally (decode2231Encinmediatype.go), but only forfilename*values whose charset token isus-asciiorutf-8:When
decode2231Encreturnsfalse, the stdlib silently leaves whatever was parsed for the plainfilenameparameter untouched (or leavesfilenameabsent entirely if onlyfilename*was supplied) — there is no error, no partial-parse indicator, nothing a caller can detect. The stdlib also discards the RFC 5987languagecomponent offilename*entirely; it is not recoverable from the parsed params map at all.Verified behavior (prior to the fix)
Content-DispositionfieldsFILES)filename="safe.jpg"; filename*=UTF-8''shell.phpshell.php(correct)filename="safe.jpg"; filename*=iso-8859-1''shell.phpsafe.jpg(decoy wins)filename*=iso-8859-1''shell.php(no plainfilename)ARGS_POSTinsteadIn the second row, any rule inspecting
FILESfor a dangerous extension only ever seessafe.jpg, while a backend that follows RFC 7578 precedence and acceptsiso-8859-1(an RFC-5987-legal, non-exotic charset) usesshell.phpas the filename. In the third row, the upload skips file-specific rule scope and size-limit tracking (FILES_COMBINED_SIZE) entirely.Impact
A rule set that blocks file uploads by extension or name via
FILES(or scopes rules toFILES/FILES_NAMES) can be bypassed by supplying the real filename viafilename*under any charset other thanutf-8/us-ascii—iso-8859-1is sufficient and is a standards-compliant choice, not an obscure or malformed one — optionally alongside an innocuous decoyfilename. A part carrying onlyfilename*additionally escapes file-vs-field routing, soFILES,FILES_NAMESandFILES_COMBINED_SIZEnever see it.The bypass is deterministic against Coraza and requires no authentication or user interaction. Its effect is bounded to filename-derived inspection: request body content,
ARGSand headers are still inspected, and a part carrying onlyfilename*is routed intoARGS_POST. Coraza itself neither stores nor serves the uploaded file, so whether an evaded rule was preventing a change of state depends on the backend's ownfilename*precedence handling and upload handling. This is scored as a scope-changing bypass with low integrity impact (S:C,I:L) rather than a direct compromise.Proof of Concept
Processed through
internal/bodyprocessors/multipart.goprior to the fix,FILESfor this part containedsafe.jpg. ASecRule FILES "\.php$"(or similar extension-blocklist rule) did not fire, while a backend honoring RFC 5987/7578filename*precedence usesshell.phpas the filename.Resolution
Fixed by locating and decoding
filename*independently ofmime.ParseMediaType's charset restriction (a quoted-string-aware scanner over the raw header). The fix also populatesMULTIPART_FILENAME/MULTIPART_NAME, which were previously declared but unpopulated.Both readings are exposed, not just
filename*. An earlier version of this fix madefilename*unconditionally authoritative over the plainfilename, per RFC 7578 §4.2 — this closed the PoC above but relocated the same bypass: swap which field carries the real name (filename="shell.php"; filename*=iso-8859-1''safe.jpg) and a backend that resolves the plainfilenameinstead is missed, since the discarded reading never reachedFILESat all. Fixed in89399e67: when a well-formedfilename*picks a different value than the plainfilename, both are added toFILES/FILES_SIZES/MULTIPART_FILENAMEfor the same part (one temp file, one byte count, two names). ASecRule FILES "\.php$"now catches the payload regardless of which field carries the real name.Coraza does not select a "safe" charset allowlist on the operator's behalf. Instead,
filename*'s declared charset and language are exposed as their own rule-matchable collections,MULTIPART_FILENAME_CHARSETandMULTIPART_FILENAME_LANGUAGE, alongsideMULTIPART_FILENAME, so operators can enforce their own allowlist. Design credit: @airween.For
Content-Disposition: form-data; name="upload"; filename="safe.jpg"; filename*=UTF-8''shell.php:Rule writers can enforce a charset allowlist directly. Use an anchored regex
rather than
@within:!@within utf-8,iso-8859-1,us-asciilooks equivalent and is not.@withintreats its parameter as the haystack and the variable as the needle, so an
empty value is trivially contained in it and the negation never fires. A
filename*that declares no charset at all —filename*=''shell.php, whichRFC 5987's grammar does not permit but which Coraza accepts and exposes rather
than rejecting — therefore passes an
@withinallowlist untouched. Measured:MULTIPART_FILENAME_CHARSET!@within utf-8,...!@rx ^(?:utf-8|...)$UTF-8shift_jisfilename*=''...)Both spellings stay quiet on a part with no
filename*at all, because thecollections are then absent rather than empty and the rule never evaluates.
That is what makes an allowlist rule safe to apply to all traffic rather than
only to known upload fields.
Discrepancies are no longer dropped silently
The same silent-fallback pattern this advisory describes existed on two neighbouring paths: a part whose
Content-Dispositionwas present but unparseable, and a part carrying afilename*that did not match thecharset'[language]'valueshape at all. Both were reduced to a part with no filename — routed intoARGS_POST, outsideFILES-scoped rule coverage, with nothing to signal that a filename had been discarded.Both now raise
MULTIPART_STRICT_ERROR(rule 200003 in CRS). A part with noContent-Dispositionat all is not flagged: it simply carries no filename.mime.ParseMediaTypeaccepts every real-world filename shape tested — raw UTF-8 and emoji, spaces, Windows backslashes, a bare%, unquoted values — so this signal does not reach legitimate uploads.New variable:
MULTIPART_DUPLICATE_PART_HEADERSet to
1when a part repeats a header (twoContent-Dispositionheaders) or repeats a parameter inside itsContent-Disposition(twofilenameparameters). A duplicate is precisely where backends disagree about which value wins, so the duplicate itself is the signal; it also contributes toMULTIPART_STRICT_ERROR.ModSecurity adds the same variable in its fix for GHSA-5pww-8rfg-9crf and lists it in the recommended audit-log format as
DH. Coraza implements it under the same name so that a rule set referencing it parses on both engines.Note on percent-decoding
Coraza percent-decodes the
filename*value, soMULTIPART_FILENAMEandFILEShold the name the application will actually resolve. This matters for rules that match on file extension: CRS 933110 and 944140 targetFILESwitht:none,t:lowercase,t:removeWhitespaceand perform no URL decoding, so an encoded separator such asfilename*=UTF-8''shell%2Ephpwould evade them if the raw value were stored instead. ModSecurity's fix stored the raw, still-encoded value when this was first written; after the difference was raised on its advisory it now percent-decodes as well, so the two engines agree on this point.Three further discrepancies found in post-fix review
Reviewing the fix (
23446b5e) against test coverage turned up three more cases where Coraza's parsed value could disagree with what a backend resolves, closed inf30df398:%escape infilename*(filename*=UTF-8''safe.jpg%ZZ) is still kept as-is rather than substituting a decoy plainfilename, but now also raisesMULTIPART_STRICT_ERROR. Without this, the discrepancy was silent whenever a backend decoded the same escape differently or rejected it outright.filename*value (filename*="UTF-8''shell.php") is unwrapped before parsing. RFC 5987 does not permitfilename*to be a quoted-string, but a general Content-Disposition parser such as Go'smime.ParseMediaTypeaccepts a quoted value for any parameter. Without unwrapping, the literal quote characters ended up inMULTIPART_FILENAME/MULTIPART_FILENAME_CHARSET, so an anchored rule (e.g.\.php$) did not match a filename the backend resolved cleanly.MULTIPART_FILENAME,MULTIPART_FILENAME_CHARSET,MULTIPART_FILENAME_LANGUAGEandMULTIPART_NAMEare multi-valued collections: two parts sharing onename(e.g. a multi-file upload field) each keep their own filename instead of the later part overwriting the earlier one. ModSecurity's fix for GHSA-5pww-8rfg-9crf independently identifies and fixes the same class of bug, describing it as "a second, distinct bypass," by keeping the equivalent variables multi-valued.The precedence decision itself was found to relocate the bypass
Reviewing
filename*-as-authoritative, @jptosso demonstrated (with a repro against Go's ownmime/multipartas the reference backend) that this precedence choice doesn't close the two-sided decoy: it only decides which of the two payload shapes above is caught, not both. Checked against ModSecurity v3's actual fix for GHSA-5pww-8rfg-9crf: it makes the identical unconditional-precedence choice (single value,filename*wins, plainfilenamediscarded), with no record of the two-sided case being considered there either — this is not a case of Coraza deviating from a more careful upstream fix.Fixed in
89399e67by keeping both readings rather than picking one (see Resolution above). Test coverage for the original PoC direction only ever exercised thefilename="safe.jpg"; filename*=...''shell.phporder; the swapped order (filename="shell.php"; filename*=...''safe.jpg) is now a dedicated regression case, since that gap in coverage is exactly why the one-sided version shipped in the first place.Patched in 3.8.1
The 3.8.0 fix was incomplete. 3.8.1 completes it: an RFC 2231 numbered continuation (
filename*0=,filename*0*=, ...) next to a barefilename*parameter now setsMULTIPART_STRICT_ERROR, and the Content-Disposition duplicate-parameter check added in 3.8.0 is now linear instead of quadratic in the number of parameters. Upgrade to 3.8.1; 3.8.0 is listed as affected.Severity (revised 2026-10-02)
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N(4.0, Medium).Attack Complexity is High because the bypass depends on a specific backend behaviour outside the attacker's control. Go's
mime.ParseMediaTypeand PHP resolve the same filename Coraza did, so they are not affected. The discrepancy exists only for backends that decode a non-UTF-8filename*or treat afilename*-only part as a file, such as Werkzeug/Flask and busboy-based Node.js backends (Express with multer, Fastify). The continuation case fixed in 3.8.1 is resolved only by Werkzeug. The previous vector scored Attack Complexity Low (5.8).Impact metrics follow the convention used across Coraza's WAF-bypass advisories: the vulnerable component is Coraza, but the impact lands on the protected application, so Scope is Changed. The bypass hides a payload from inspection; the application still has to be vulnerable to it, so Integrity is Low and Confidentiality is not scored separately.
AI involvement in this section: Claude Opus 5.5 (Anthropic), via Claude Code, re-derived the CVSS vector from the project's triage guidance (AGENTS.md, "CVSS preconditions get verified, not copied from the report") and drafted this text. A human maintainer (fzipi) chose the
S:C/I:Limpact convention and directed this update.