GHSA-x26q-wvhg-fh4m
Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations
Packages
| Ecosystem | Package | Affected | Fixed in |
|---|---|---|---|
| Go | github.com/corazawaf/coraza/v3 | >= 3.0.0, < 3.8.0 | 3.8.0 |
Details
As written in the advisory (Markdown source, shown as plain text).
## Root Cause
File: `internal/corazawaf/transaction.go`, lines 834–866.
```go
parsedURL, err := url.ParseRequestURI(uri)
query := ""
if err != nil {
tx.variables.urlencodedError.Set(err.Error())
path = uri
tx.variables.requestURI.Set(uri)
/*
tx.Variables.VARIABLE_URI_PARSE_ERROR.Set("1")
posRawQuery := strings.Index(uri, "?")
if posRawQuery != -1 {
tx.ExtractArguments("GET", uri[posRawQuery+1:])
path = uri[:posRawQuery]
query = uri[posRawQuery+1:]
} else {
path = uri
}
tx.Variables.RequestUri.Set(uri)
*/
} else {
tx.ExtractGetArguments(parsedURL.RawQuery) // only path that populates ARGS_GET
tx.variables.requestURI.Set(parsedURL.String())
path = parsedURL.Path
query = parsedURL.RawQuery
}
...
tx.variables.queryString.Set(query)
```
When `url.ParseRequestURI(uri)` returns an error — which Go's stdlib does for any URI containing raw control bytes (`\x00`, `\n`, `\r`, `\t`, other `0x00–0x1F`, `0x7F`) — the error branch silently produces an empty `QUERY_STRING` and an empty `ARGS_GET` collection. The fallback logic that should split on `?` and populate the GET arguments from the raw tail is already present in the source as a commented-out block, referencing a `VARIABLE_URI_PARSE_ERROR` variable that was never wired up.
Consequences on the error branch:
- `ARGS_GET` / `ARGS_GET_NAMES` / `ARGS` (union) are **empty** — `ExtractGetArguments` is never called.
- `QUERY_STRING` is **empty** (initial `query := ""` at line 835 persists through to `queryString.Set(query)` at line 866).
- `REQUEST_FILENAME` / `REQUEST_BASENAME` contain the entire URI including any `?…` query suffix (because `path = uri` at line 838 bypasses the parse, and the subsequent `strings.LastIndexAny(path, "/\\")` runs over the raw URI).
- `URLENCODED_ERROR` is set to the Go error message. That variable is *also* set by the urlencoded body processor on body-decode failures, so an operator cannot distinguish "malformed URI" from "malformed request body" without string-matching the error text.
- `REQUEST_URI_RAW` (set unconditionally at line 822, before the parse) **is** populated correctly.
Any rule targeting `ARGS_GET`, `ARGS`, `ARGS_NAMES`, `ARGS_GET_NAMES`, or `QUERY_STRING` — which is the default target set for the vast majority of OWASP CRS GET-side signature rules — does not fire against attacker content that reaches Coraza via a URI Go's `net/url` rejects.
## Reachability
This issue **does not affect the standard `coraza/v3/http` + `net/http` integration**. Go's `http.ReadRequest` calls `url.ParseRequestURI` first and rejects malformed URIs with `400 Bad Request` before `ProcessURI` is invoked. Verified experimentally against a Coraza-wrapped `net/http` server — a raw request with a control-byte-laced URI produced `HTTP 400`, and the handler was never reached.
The bug is reachable when an integration forwards raw URI bytes to `tx.ProcessURI` directly, bypassing Go's HTTP parser:
- **`coraza-spoa`** — HAProxy SPOP agent. Receives URI from HAProxy, which permits bytes `net/http` rejects.
- **`coraza-proxy-wasm`** — Envoy WASM filter. Passes the `:path` pseudo-header from Envoy.
- Custom FFI/WASM hosts and any embedder calling `tx.ProcessURI(rawURI, method, httpVersion)` with bytes not pre-validated by Go's URL parser.
This gates the attack to Attack Complexity:High — a standard Go HTTP deployment is not exposed.
## Proof of Concept
Direct-API reproduction (simulating the non-net/http integration path):
```go
waf, _ := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(`
SecRuleEngine On
SecRule ARGS_GET "@contains ATTACK_HERE_XYZ" "id:9001,phase:1,deny,status:403"
SecRule QUERY_STRING "@contains ATTACK_HERE_XYZ" "id:9002,phase:1,deny,status:403"
`))
for _, uri := range []string{
"/search?q=ATTACK_HERE_XYZ", // baseline
"/search?q=ATTACK_HERE_XYZ\x00&y=1", // NUL byte
"/search?q=ATTACK_HERE_XYZ\ninjected: header", // bare LF
"/search?q=ATTACK_HERE_XYZ\rhdr: x", // bare CR
"/search?q=ATTACK_HERE_XYZ\tx=1", // tab
} {
tx := waf.NewTransaction()
tx.ProcessURI(uri, "GET", "HTTP/1.1")
it := tx.ProcessRequestHeaders()
// inspect tx.Variables().QueryString().Get() and tx.Variables().ArgsGet().FindAll()
tx.Close()
}
```
Observed:
| URI | `QUERY_STRING` | `ARGS_GET` | interrupted? |
|---|---|---|---|
| `/search?q=ATTACK_HERE_XYZ` | `q=ATTACK_HERE_XYZ` | 1 entry | **yes (403)** |
| `/search?q=ATTACK_HERE_XYZ\x00&y=1` | `""` | 0 entries | **no — BYPASS** |
| `/search?q=ATTACK_HERE_XYZ\ninjected: header` | `""` | 0 entries | **no — BYPASS** |
| `/search?q=ATTACK_HERE_XYZ\rhdr: x` | `""` | 0 entries | **no — BYPASS** |
| `/search?q=ATTACK_HERE_XYZ\tx=1` | `""` | 0 entries | **no — BYPASS** |
`REQUEST_URI_RAW` is populated correctly in every case (line 822 sets it before the parse), so a rule written against `REQUEST_URI_RAW` still catches the attack. CRS and most operator-written rules target `ARGS_GET` / `ARGS` / `QUERY_STRING` — those do not fire.
HTTP-layer reachability check (stock `net/http`):
```
$ printf 'GET /?q=ATTACK_HERE_XYZ\x00&y=1 HTTP/1.1\r\nHost: x\r\n\r\n' | nc 127.0.0.1 8092
HTTP/1.1 400 Bad Request
```
Confirms the exposure is limited to non-net/http integrations.
## Mitigation
Recommended fixes, in order:
### 1. Re-enable the existing fallback and wire up `URI_PARSE_ERROR`
The code to fix this is already present as a commented-out block at `transaction.go:840–851`. Re-enable it, promote the referenced `VARIABLE_URI_PARSE_ERROR` to a real transaction variable, and populate `ARGS_GET` / `QUERY_STRING` from the raw `?…` tail:
```go
if err != nil {
tx.variables.urlencodedError.Set(err.Error())
tx.variables.uriParseError.Set("1") // new variable
tx.variables.requestURI.Set(uri)
if i := strings.Index(uri, "?"); i != -1 {
path = uri[:i]
query = uri[i+1:]
tx.ExtractGetArguments(query) // populate ARGS_GET
} else {
path = uri
}
} else {
...
}
```
### 2. Ship a companion rule in `coraza.conf-recommended`
```conf
SecRule URI_PARSE_ERROR "@eq 1" \
"id:'200010',phase:1,t:none,log,deny,status:400,msg:'URI failed to parse'"
```
This gives operators a fail-closed default (analogous to rule 200003 for multipart strict error and rule 200002 for body-parse error), so non-net/http integrations at least stop the request regardless of downstream rule coverage.
### 3. Do not overload `URLENCODED_ERROR`
The current code uses `URLENCODED_ERROR` for URI parse failures. That variable is also set by the urlencoded body processor on body-decode errors; operators cannot distinguish the two causes without string-matching the error text, and any rule they add will fire on both classes of failure. A dedicated `URI_PARSE_ERROR` variable (per the commented-out TODO) is the right shape.
## Affected versions
All releases on the v3 branch (`>= 3.0.0, <= 3.7.0`); the silent-drop behavior has been present since the first v3 release. Only deployments using non-net/http integrations (coraza-spoa, coraza-proxy-wasm, custom FFI) are exposed in practice.
## References
- `internal/corazawaf/transaction.go` lines 834–866 (ProcessURI error branch)
- `internal/corazawaf/transaction.go` line 822 (`REQUEST_URI_RAW` is populated before the parse, which is why `REQUEST_URI_RAW`-targeted rules still catch the attack)
- Commented-out fallback at lines 840–851 referencing `VARIABLE_URI_PARSE_ERROR`
- CWE-20 — Improper Input Validation
- CWE-436 — Interpretation Conflict
### 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 stays High: the bypass only applies to integrations that pass Coraza a raw URI that Go's URL parser rejects, which `net/http` does not. The previous vector scored Integrity High (6.8); it is scored here like Coraza's other inspection bypasses.
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._
References
External links from the advisory. They lead to third-party sites.