Vulnerability GHSA-x26q-wvhg-fh4m

Medium Risk
MEDIUM RISK
CVSS Score: 4.0
Score Range: 4.0–6.9
Medium severity vulnerabilities (CVSS 4.0–6.9). Important issues that meaningfully reduce security confidence.
5 hours ago
October 08, 2026 at 05:46 PM UTC
Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations
v3.0.0 - v3.7.0
v3.0.0 - v3.7.0

Summary

Coraza: ProcessURI silently drops QUERY_STRING and ARGS_GET on URI parse failure — defense-in-depth bypass for non-net/http integrations

Details

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 — 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):

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.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.

Timeline

Published
5 hours ago
October 08, 2026 at 05:46 PM UTC
Last Modified
5 hours ago
October 08, 2026 at 06:00 PM UTC