GHSA-X26Q-WVHG-FH4M
Vulnerability from github – Published: 2026-10-08 17:46 – Updated: 2026-10-09 17:32Root Cause
File: internal/corazawaf/transaction.go, lines 834–866.
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 —ExtractGetArgumentsis never called.QUERY_STRINGis empty (initialquery := ""at line 835 persists through toqueryString.Set(query)at line 866).REQUEST_FILENAME/REQUEST_BASENAMEcontain the entire URI including any?…query suffix (becausepath = uriat line 838 bypasses the parse, and the subsequentstrings.LastIndexAny(path, "/\\")runs over the raw URI).URLENCODED_ERRORis 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 bytesnet/httprejects.coraza-proxy-wasm— Envoy WASM filter. Passes the:pathpseudo-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):
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:
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
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.golines 834–866 (ProcessURI error branch)internal/corazawaf/transaction.goline 822 (REQUEST_URI_RAWis populated before the parse, which is whyREQUEST_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.
{
"affected": [
{
"package": {
"ecosystem": "Go",
"name": "github.com/corazawaf/coraza/v3"
},
"ranges": [
{
"events": [
{
"introduced": "3.0.0"
},
{
"fixed": "3.8.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-107825"
],
"database_specific": {
"cwe_ids": [
"CWE-20",
"CWE-436"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-08T17:46:00Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "## Root Cause\n\nFile: `internal/corazawaf/transaction.go`, lines 834\u2013866.\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 \u2014 which Go\u0027s stdlib does for any URI containing raw control bytes (`\\x00`, `\\n`, `\\r`, `\\t`, other `0x00\u20130x1F`, `0x7F`) \u2014 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** \u2014 `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 `?\u2026` 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` \u2014 which is the default target set for the vast majority of OWASP CRS GET-side signature rules \u2014 does not fire against attacker content that reaches Coraza via a URI Go\u0027s `net/url` rejects.\n\n## Reachability\n\nThis issue **does not affect the standard `coraza/v3/http` + `net/http` integration**. Go\u0027s `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 \u2014 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\u0027s HTTP parser:\n\n- **`coraza-spoa`** \u2014 HAProxy SPOP agent. Receives URI from HAProxy, which permits bytes `net/http` rejects.\n- **`coraza-proxy-wasm`** \u2014 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\u0027s URL parser.\n\nThis gates the attack to Attack Complexity:High \u2014 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\u0026y=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\u0026y=1` | `\"\"` | 0 entries | **no \u2014 BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\ninjected: header` | `\"\"` | 0 entries | **no \u2014 BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\rhdr: x` | `\"\"` | 0 entries | **no \u2014 BYPASS** |\n| `/search?q=ATTACK_HERE_XYZ\\tx=1` | `\"\"` | 0 entries | **no \u2014 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` \u2014 those do not fire.\n\nHTTP-layer reachability check (stock `net/http`):\n\n```\n$ printf \u0027GET /?q=ATTACK_HERE_XYZ\\x00\u0026y=1 HTTP/1.1\\r\\nHost: x\\r\\n\\r\\n\u0027 | 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\u2013851`. Re-enable it, promote the referenced `VARIABLE_URI_PARSE_ERROR` to a real transaction variable, and populate `ARGS_GET` / `QUERY_STRING` from the raw `?\u2026` 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:\u0027200010\u0027,phase:1,t:none,log,deny,status:400,msg:\u0027URI failed to parse\u0027\"\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 (`\u003e= 3.0.0, \u003c= 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\u2013866 (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\u2013851 referencing `VARIABLE_URI_PARSE_ERROR`\n- CWE-20 \u2014 Improper Input Validation\n- CWE-436 \u2014 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\u0027s URL parser rejects, which `net/http` does not. The previous vector scored Integrity High (6.8); it is scored here like Coraza\u0027s other inspection bypasses.\n\nImpact metrics follow the convention used across Coraza\u0027s 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\u0027s 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._",
"id": "GHSA-x26q-wvhg-fh4m",
"modified": "2026-10-09T17:32:28Z",
"published": "2026-10-08T17:46:00Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/corazawaf/coraza/security/advisories/GHSA-x26q-wvhg-fh4m"
},
{
"type": "WEB",
"url": "https://github.com/corazawaf/coraza/commit/0321af96cef18fbafb40980cf075d7cc449a66fa"
},
{
"type": "PACKAGE",
"url": "https://github.com/corazawaf/coraza"
},
{
"type": "WEB",
"url": "https://github.com/corazawaf/coraza/releases/tag/v3.8.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure \u2014 defense-in-depth bypass for non-net/http integrations"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.