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

## Overview

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

## Affected packages

- `github.com/corazawaf/coraza/v3 >= 3.0.0, < 3.8.0`

## Remediation

Upgrade to a patched release:

- `github.com/corazawaf/coraza/v3 3.8.0`
