Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection
Description
### Summary Coraza fails to inspect URL-encoded form bodies when their valid `Content-Type` contains a media-type parameter, for example: ```http Content-Type: application/x-www-form-urlencoded; charset=UTF-8 ``` An unauthenticated attacker can use this header to hide the complete form body from custom Coraza rules that inspect `ARGS_POST` or `REQUEST_BODY`, while the bundled Go HTTP middleware forwards the body and the backend parses it normally. This is a deterministic request-body inspection bypass. Suggested severity: **Medium**. Current OWASP CRS includes rule `901340`, which forces fallback inspection and mitigates this path; this report does not claim a bypass of an unmodified current CRS ruleset ### Details The affected component is request-body processor selection in [`internal/corazawaf/transaction.go`](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/corazawaf/transaction.go#L371-L390). `Transaction.AddRequestHeader` compares the complete lowercased header value using exact equality: ```go case "content-type": val := strings.ToLower(value) if val == "application/x-www-form-urlencoded" { tx.variables.reqbodyProcessor.Set("URLENCODED") } else if strings.HasPrefix(val, "multipart/form-data") { tx.variables.reqbodyProcessor.Set("MULTIPART") } ``` Source: [`internal/corazawaf/transaction.go`, lines 383-390](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/corazawaf/transaction.go#L383-L390). The media type of `application/x-www-form-urlencoded; charset=UTF-8` remains `application/x-www-form-urlencoded`; `charset` is a parameter. Because Coraza compares the entire header, the comparison fails and `REQBODY_PROCESSOR` remains empty. `ProcessRequestBody` treats the empty processor as success: ```go rbp = strings.ToLower(rbp) if rbp == "" { tx.WAF.Rules.Eval(types.PhaseRequestBody, tx) return tx.interruption, nil } ``` Source: [`internal/corazawaf/transaction.go`, lines 1115-1120](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/corazawaf/transaction.go#L1115-L1120). Coraza therefore evaluates phase 2 without parsing the body and without setting `REQBODY_ERROR`. The [URL-encoded processor that normally populates `ARGS_POST`, `REQUEST_BODY`, and `REQUEST_BODY_LENGTH`](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/internal/bodyprocessors/urlencoded.go#L19-L33) is never invoked. The [bundled middleware buffers and reconstructs the full request body before invoking the backend](https://github.com/corazawaf/coraza/blob/db9850b2dd8992f97a8cefe08d0cb4edd966a04c/http/middleware.go#L69-L97). The backend consequently receives data that was absent from Coraza's body variables. The issue exists in the standard compiled implementation; no special build tag is required. Request-body access defaults to Off in an empty configuration, but Coraza's recommended configuration enables it with `SecRequestBodyAccess On`. ### PoC Save this self-contained test as `parameterized_form_bypass_test.go` in the Coraza repository root: ```go package coraza_test import ( "net/http" "net/http/httptest" "strings" "testing" coraza "github.com/corazawaf/coraza/v3" corazahttp "github.com/corazawaf/coraza/v3/http" ) func TestParameterizedFormInspectionBypass(t *testing.T) { cfg := coraza.NewWAFConfig().WithDirectives(` SecRuleEngine On SecRequestBodyAccess On SecRule ARGS_POST:cmd "@streq evil" "id:910001,phase:2,deny,status:403,t:none" `) waf, err := coraza.NewWAF(cfg) if err != nil { t.Fatal(err) } backendSaw := "" handler := corazahttp.WrapHandler(waf, http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { t.Fatal(err) } backendSaw = r.PostForm.Get("cmd") w.WriteHeader(http.StatusNoContent) }, )) req := httptest.NewRequest( http.MethodPost, "http://example.test/", strings.NewReader("cmd=evil"), ) req.Header.Set( "Content-Type", "application/x-www-form-urlencoded; charset=UTF-8", ) rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusNoContent { t.Fatalf("expected request to reach backend, status=%d", rec.Code) } if backendSaw != "evil" { t.Fatalf("backend did not receive malicious value: %q", backendSaw) } } ``` Run: ```bash go test . -run TestParameterizedFormInspectionBypass -v ``` Observed: ```text === RUN TestParameterizedFormInspectionBypass --- PASS: TestParameterizedFormInspectionBypass (0.00s) PASS ``` The test passes only when Coraza fails to return its configured 403 response and the backend parses `cmd=evil`. As a control, change the header to: ```http Content-Type: application/x-www-form-urlencoded ``` Coraza then selects the URL-encoded processor, exposes `cmd=evil` as `ARGS_POST:cmd`, and returns 403 before the backend runs. ### Impact This is a parser differential and request-body inspection bypass affecting applications protected by custom Coraza rules. An unauthenticated attacker can add `charset=UTF-8` to an ordinary form request. Coraza then runs phase-2 rules without the submitted parameters and reports no body-processing failure, while the application receives and processes those parameters. Rules relying on these variables are affected on this path: - `ARGS_POST` - `ARGS` for POST-derived values - `ARGS_POST_NAMES` - `ARGS_NAMES` - `REQUEST_BODY` - `REQUEST_BODY_LENGTH` This defeats custom rules intended to reject injection strings, dangerous commands, or forbidden business values in form fields. The final application impact depends on the backend behavior that the bypassed rule was intended to protect. Affected operators are those who enable request-body access and rely on automatic URL-encoded parsing without current CRS rule `901340`, `ctl:forceRequestBodyVariable=On`, an explicitly forced processor, or equivalent backend validation. The fix is to parse Content-Type according to MIME syntax and compare the normalized media type without parameters, for example with Go's `mime.ParseMediaType`. If an expected body has no processor, Coraza should expose an explicit processing error instead of silently evaluating phase 2 with empty variables. ## Follow-up (2026-09-30): duplicate Content-Type headers desynchronize processor selection from the parsed body The original fix made body-processor selection tolerant of media-type parameters (`charset=UTF-8` etc.) by switching from exact equality to `strings.HasPrefix`. A second, distinct gap was found while reviewing that same selection code: it never accounted for a request carrying *more than one* `Content-Type` header. ### Root cause `Transaction.AddRequestHeader` is called once per header by the integrator (documented contract). Its `content-type` case runs on every call: ```go case "content-type": val := strings.ToLower(value) if strings.HasPrefix(val, "application/x-www-form-urlencoded") { tx.variables.reqbodyProcessor.Set("URLENCODED") } else if strings.HasPrefix(val, "multipart/form-data") { tx.variables.reqbodyProcessor.Set("MULTIPART") } ``` A request with two `Content-Type` headers therefore has its body-processor selection silently overwritten by the *last* one, since each call unconditionally calls `Set`. Meanwhile, `ProcessRequestBody` extracts the `mimeType` it passes to whichever processor gets selected from `requestHeaders.Get("content-type")[0]` -- the *first* value (`internal/corazawaf/transaction.go`, a few hundred lines below `AddRequestHeader`) -- and Go's `net/http` `Header.Get` (what a typical backend uses) also returns only the first value. So selection follows the last header while everything else follows the first, and the two can disagree entirely. ### PoC ```http Content-Type: multipart/form-data; boundary=XyZ Content-Type: application/x-www-form-urlencoded ``` with a genuine multipart body containing `cmd=evil` and a `shell.php` file upload. Against commit `19b86824`: - `reqbodyProcessor` resolves to `"URLENCODED"` (the second, spoofed header wins). - The URL-encoded processor runs against the raw multipart body text, parses without error, and produces nothing. - `ARGS_POST:cmd`, `FILES`, and `FILES_NAMES` are all empty -- the entire payload is invisible to any rule inspecting those variables. - The backend (or any integrator using `Header.Get`, which reads the first value) still sees `Content-Type: multipart/form-data` and parses `cmd=evil` and the uploaded `shell.php` normally. Current OWASP CRS includes rule `920620` (`Content-Type` header sanity check via count/format), which catches this shape; the recommended `coraza.conf-recommended` has no equivalent, so a Coraza deployment with only custom rules (no CRS) is exposed. ### Fix Only the first `Content-Type` header may set `reqbodyProcessor`: guard the existing logic with `if tx.variables.reqbodyProcessor.Get() == ""`. This makes header-driven processor selection consistent with `ProcessRequestBody`'s own `mimeType` extraction and with standard `Header.Get` semantics, without a new field (nothing else can set `reqbodyProcessor` before headers finish processing; `ctl:requestBodyProcessor` and `ForceRequestBodyVariable` both run afterward, in phase 1 rule evaluation and body processing respectively, and are unaffected). As defense in depth, a rule author can also detect the anomaly directly today, with no code change: `SecRule &REQUEST_HEADERS:Content-Type "@gt 1" "deny,..."` -- left as a suggested addition to `coraza.conf-recommended` rather than bundled into this fix, since it is a policy choice (deny vs. flag) rather than a correctness requirement. ### AI involvement disclosure - **AI tools/models used:** Claude Sonnet 5 (Anthropic), via Claude Code. - **What was generated/assisted:** the vulnerability hypothesis and repro shape were supplied by the reporter as an existing written finding; Claude Sonnet 5 independently re-derived the root cause by reading the current source, traced the `mimeType`/`Header.Get` first-value asymmetry, wrote and ran a fresh PoC against commit `19b86824` confirming the full bypass (`ARGS_POST`/`FILES`/`FILES_NAMES` all empty), verified the fix closes it, and drafted this addendum. - **Review performed:** reproduced by hand against a clean checkout of commit `19b86824` before and after the fix using the real `Transaction` API (`AddRequestHeader` x2, `WriteRequestBody`, `ProcessRequestBody`); confirmed the regression test fails deterministically against the pre-fix code and passes after it; ran the full test suite, the build-tag matrix (`coraza.no_memoize`, `coraza.rule.multiphase_evaluation`, `coraza.rule.no_regex_multiline`), and the `testing/coreruleset` CRS regression suite, all green; reviewed by a human maintainer (fzipi) before this addendum was submitted. Fix: https://github.com/corazawaf/coraza-ghsa-w253-m66g-rx24/pull/2 ### Patched in 3.8.1 The 3.8.0 fix was incomplete. 3.8.1 completes it: only the first `Content-Type` header now selects the body processor (the one a typical backend reads with `Header.Get`), and leading whitespace in the media type, including Unicode whitespace that `net/http` accepts, is trimmed the way `mime.ParseMediaType` trims it. Upgrade to 3.8.1; 3.8.0 is listed as affected. ### Severity (revised 2026-10-02) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:N` (5.8, Medium). Attack Complexity is Low: every form and multipart parser accepts these Content-Type values. The previous vector used Scope Unchanged (5.3). 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._
CVSS v3.1 base metrics
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:N/I:L/A:NMedium severity
Band computed from the CVSS base score, not the source's own label — so it means the same thing across every feed.
AV
Network
Attack Vector
AC
Low
Attack Complexity
PR
None
Privileges Required
UI
None
User Interaction
S
Changed
Scope
C
None
Confidentiality
I
Low
Integrity
A
None
Availability
Affected
- Vendor
- Go
- Product
- github.com/corazawaf/coraza/v3
Versions
- pkg:golang/github.com/corazawaf/coraza/v3 >= 3.0.4, < 3.8.1
Stated as the source expressed them.
References
- advisoryOSV GHSA-w253-m66g-rx24https://osv.dev/vulnerability/GHSA-w253-m66g-rx24
- otherOSV webhttps://github.com/corazawaf/coraza/security/advisories/GHSA-w253-m66g-rx24
- otherOSV webhttps://github.com/corazawaf/coraza/commit/3b6241ef895b089c66a59e51068a81cf40986e4d
- otherOSV webhttps://github.com/corazawaf/coraza/commit/a55950ffa161f33a29e14f87d926d7df3b85cc74
- otherOSV webhttps://github.com/corazawaf/coraza/commit/dd100261cf1d2e2b053803a50c9db28dcaaa4b7c
- vendorOSV packagehttps://github.com/corazawaf/coraza
- otherOSV webhttps://github.com/corazawaf/coraza/releases/tag/v3.8.1
Related threats
same CWE or vendorCVE-2026-107726 — Hazelcast is a unified real-time data platform combining stream processing with a fast data store.
CVE-2026-107726 · 57m ago
CVE-2026-107720 — fast-jwt provides fast JSON Web Token (JWT) implementation.
CVE-2026-107720 · 57m ago
CVE-2026-107717 — Banks generates meaningful LLM prompts using a simple template language.
CVE-2026-107717 · 57m ago
CVE-2026-107386 — amqp091-go is a Go AMQP 0.9.1 client.
CVE-2026-107386 · 4h ago
CVE-2026-106430 — The MongoDB C++ Driver discards content after an embedded NUL byte in certain field and collection names accepted by the collection API.
CVE-2026-106430 · 4h ago
Coraza: Resource exhaustion via deferred file handle accumulation in multipart body processor
OSV · 5h ago