Know every vulnerabilitybefore it knows you.
DevGuard continuously monitors your dependencies and alerts you when CVEs like this one affect your stack — with real-time threat intelligence built for developers.
GHSA-x26q-wvhg-fh4m
Root 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.
Upload your own SBOM in CycloneDX 1.6 or higher (JSON) directly here to check your vulnerabilities.
Drag and drop some file here, or click to select
The vulnerability can be exploited over the network without needing physical access. It is difficult for an attacker to exploit this vulnerability and may require special conditions. An attacker does not need any special privileges or access rights. No user interaction is needed for the attacker to exploit this vulnerability. The vulnerability can affect other systems as well, not just the initial system. There is a low impact on the integrity of the data.
Limited exploitation activity has been observed. Close monitoring and planned remediation are recommended.
The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database.
Browse More
Continuously monitor your dependencies and get alerted when vulnerabilities like this one affect your stack.
Checkout DevGuard