Summary

Coraza: Multipart filename* (RFC 5987) charset restriction lets a decoy filename bypass FILES-based rules

Advisory details

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

quot; (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

quot; 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)
quot; \ "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 fil

References