{"id":"GHSA-x26q-wvhg-fh4m","title":"Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations","summary":"Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations","severity":"medium","cvss":4,"cwe":["CWE-20","CWE-436"],"vendor":"corazawaf","product":"github.com/corazawaf/coraza/v3","ecosystem":"go","affected":["github.com/corazawaf/coraza/v3 >= 3.0.0, < 3.8.0"],"patched":["github.com/corazawaf/coraza/v3 3.8.0"],"published":"2026-10-08","updated":"2026-10-08","sourceUpdated":"2026-10-08T17:46:00Z","source":"GHSA","sourceUrl":"https://github.com/advisories/GHSA-x26q-wvhg-fh4m","references":[{"url":"https://github.com/corazawaf/coraza/security/advisories/GHSA-x26q-wvhg-fh4m"},{"url":"https://github.com/corazawaf/coraza/commit/0321af96cef18fbafb40980cf075d7cc449a66fa"},{"url":"https://github.com/corazawaf/coraza/releases/tag/v3.8.0"},{"url":"https://github.com/advisories/GHSA-x26q-wvhg-fh4m"}],"tags":["ghsa","go"],"ingestedAt":"2026-10-08T17:56:11.722Z","slug":"GHSA-x26q-wvhg-fh4m","body":"## Overview\n\n## Root Cause\n\nFile: `internal/corazawaf/transaction.go`, lines 834–866.\n\n```go\nparsedURL, err := url.ParseRequestURI(uri)\nquery := \"\"\nif err != nil {\n    tx.variables.urlencodedError.Set(err.Error())\n    path = uri\n    tx.variables.requestURI.Set(uri)\n    /*\n        tx.Variables.VARIABLE_URI_PARSE_ERROR.Set(\"1\")\n        posRawQuery := strings.Index(uri, \"?\")\n        if posRawQuery != -1 {\n            tx.ExtractArguments(\"GET\", uri[posRawQuery+1:])\n            path = uri[:posRawQuery]\n            query = uri[posRawQuery+1:]\n        } else {\n            path = uri\n        }\n        tx.Variables.RequestUri.Set(uri)\n    */\n} else {\n    tx.ExtractGetArguments(parsedURL.RawQuery)   // only path that populates ARGS_GET\n    tx.variables.requestURI.Set(parsedURL.String())\n    path = parsedURL.Path\n    query = parsedURL.RawQuery\n}\n...\ntx.variables.queryString.Set(query)\n```\n\nWhen `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.\n\nConsequences on the error branch:\n\n- `ARGS_GET` / `ARGS_GET_NAMES` / `ARGS` (union) are **empty** — `ExtractGetArguments` is never called.\n- `QUERY_STRING` is **empty** (initial `query := \"\"` at line 835 persists through to `queryString.Set(query)` at line 866).\n- `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).\n- `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.\n- `REQUEST_URI_RAW` (set unconditionally at line 822, before the parse) **is** populated correctly.\n\nAny 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.\n\n## Reachability\n\nThis 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.\n\nThe bug is reachable when an integration forwards raw URI bytes to `tx.ProcessURI` directly, bypassing Go's HTTP parser:\n\n- **`coraza-spoa`** — HAProxy SPOP agent. Receives URI from HAProxy, which permits bytes `net/http` rejects.\n- **`coraza-proxy-wasm`** — Envoy WASM filter. Passes the `:path` pseudo-header from Envoy.\n- Custom FFI/WASM hosts and any embedder calling `tx.ProcessURI(rawURI, method, httpVersion)` with bytes not pre-validated by Go's URL parser.\n\nThis gates the attack to Attack Complexity:High — a standard Go HTTP deployment is not exposed.\n\n## Proof of Concept\n\nDirect-API reproduction (simulating the non-net/http integration path):\n\n```go\nwaf, _ := coraza.NewWAF(coraza.NewWAFConfig().WithDirectives(`\nSecRuleEngine On\nSecRule ARGS_GET     \"@contains ATTACK_HERE_XYZ\" \"id:9001,phase:1,deny,status:403\"\nSecRule QUERY_STRING \"@contains ATTACK_HERE_XYZ\" \"id:9002,phase:1,deny,status:403\"\n`))\n\nfor _, uri := range []string{\n    \"/search?q=ATTACK_HERE_XYZ\",                    // baseline\n    \"/search?q=ATTACK_HERE_XYZ\\x00&y=1\",            // NUL byte\n    \"/search?q=ATTACK_HERE_XYZ\\ninjected: header\",  // bare LF\n    \"/search?q=ATTACK_HERE_XYZ\\rhdr: x\",            // bare CR\n    \"/search?q=ATTACK_HERE_XYZ\\tx=1\",               // tab\n} {\n    tx := waf.NewTransaction()\n    tx.ProcessURI(uri, \"GET\", \"HTTP/1.1\")\n    it := tx.ProcessRequestHeaders()\n    // inspect tx.Variables().QueryString().Get() and tx.Variables().ArgsGet().FindAll()\n    tx.Close()\n}\n```\n\nObserved:\n\n| URI | `QUERY_STRING` | `ARGS_GET` | interrupted? |\n|---|---|---|---|\n| `/search?q=ATTACK_HERE_XYZ` | `q=ATTACK_HERE_XYZ` | 1 entry | **yes (403)** |\n| `/search?q=ATTACK_HERE_XYZ\\x00&y=1` | `\"\"` | 0 entries | **no — BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\ninjected: header` | `\"\"` | 0 entries | **no — BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\rhdr: x` | `\"\"` | 0 entries | **no — BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\tx=1` | `\"\"` | 0 entries | **no — BYPASS** |\n\n`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.\n\nHTTP-layer reachability check (stock `net/http`):\n\n```\n$ 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\nHTTP/1.1 400 Bad Request\n```\n\nConfirms the exposure is limited to non-net/http integrations.\n\n## Mitigation\n\nRecommended fixes, in order:\n\n### 1. Re-enable the existing fallback and wire up `URI_PARSE_ERROR`\n\nThe 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:\n\n```go\nif err != nil {\n    tx.variables.urlencodedError.Set(err.Error())\n    tx.variables.uriParseError.Set(\"1\")               // new variable\n    tx.variables.requestURI.Set(uri)\n    if i := strings.Index(uri, \"?\"); i != -1 {\n        path = uri[:i]\n        query = uri[i+1:]\n        tx.ExtractGetArguments(query)                  // populate ARGS_GET\n    } else {\n        path = uri\n    }\n} else {\n    ...\n}\n```\n\n### 2. Ship a companion rule in `coraza.conf-recommended`\n\n```conf\nSecRule URI_PARSE_ERROR \"@eq 1\" \\\n    \"id:'200010',phase:1,t:none,log,deny,status:400,msg:'URI failed to parse'\"\n```\n\nThis 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.\n\n### 3. Do not overload `URLENCODED_ERROR`\n\nThe 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.\n\n## Affected versions\n\nAll 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.\n\n## References\n\n- `internal/corazawaf/transaction.go` lines 834–866 (ProcessURI error branch)\n- `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)\n- Commented-out fallback at lines 840–851 referencing `VARIABLE_URI_PARSE_ERROR`\n- CWE-20 — Improper Input Validation\n- CWE-436 — Interpretation Conflict\n\n### Severity (revised 2026-10-02)\n\n`CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N` (4.0, Medium).\n\nAttack 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.\n\nImpact 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.\n\n_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._\n\n## Affected packages\n\n- `github.com/corazawaf/coraza/v3 >= 3.0.0, < 3.8.0`\n\n## Remediation\n\nUpgrade to a patched release:\n\n- `github.com/corazawaf/coraza/v3 3.8.0`","depth":"sunlit","depthScore":22,"depthScoreParts":{"impact":22,"likelihood":0,"exploitation":0,"ransomware":0},"changes":[]}