GitHub Advisoryadvisory · Go
GHSA-w253-m66g-rx24 Coraza: URL-encoded form Content-Type parameters bypass Coraza body inspection
MEDIUM
Official page ↗
- Ecosystem
- Go
- Packages
- github.com/corazawaf/coraza/v3
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/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.0.4, < 3.8.1 → fixed in 3.8.1
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`.
`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.
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.
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` is never invoked.
The bundled middleware buffers and reconstructs the full request body before invoking the backend. 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 c…
Related
References
- https://github.com/corazawaf/coraza/security/advisories/GHSA-w253-m66g-rx24 web
- https://github.com/corazawaf/coraza/commit/3b6241ef895b089c66a59e51068a81cf40986e4d web
- https://github.com/corazawaf/coraza/commit/a55950ffa161f33a29e14f87d926d7df3b85cc74 web
- https://github.com/corazawaf/coraza/commit/dd100261cf1d2e2b053803a50c9db28dcaaa4b7c web
- https://github.com/corazawaf/coraza package
- https://github.com/corazawaf/coraza/releases/tag/v3.8.1 web