GitHub Advisoryadvisory · Go
GHSA-3wr7-993q-jrff Coraza: Multipart filename* (RFC 5987) charset restriction lets a decoy filename bypass FILES-based rules
MEDIUM
Official page ↗
- Ecosystem
- Go
- Packages
- github.com/corazawaf/coraza/v3
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N
- Weaknesses
- CWE-20, CWE-436
- Reviewed
- 2026-10-08
Affected packages
• Go github.com/corazawaf/coraza/v3: < 3.8.1 → fixed in 3.8.1
Description
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`:
```go
// 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.ParseMediaTyp…
Related
References
- https://github.com/corazawaf/coraza/security/advisories/GHSA-3wr7-993q-jrff web
- https://github.com/corazawaf/coraza/commit/5427c501b2fea6ae1305903efef01b3049224a58 web
- https://github.com/corazawaf/coraza/commit/b2a9264437116bdd98a86fe345b2aec1152d3d7c web
- https://github.com/corazawaf/coraza/commit/cae3c7407e7b84372c207033de03f15f89bf351a web
- https://github.com/corazawaf/coraza package
- https://github.com/corazawaf/coraza/releases/tag/v3.8.1 web