Back to database
Soon55MediumVulnerabilityNo patch link observed

Coraza has Cookie Parser Confusion

Published Oct 8, 2026, 05:51 PM UTCIngested 6h agoSource OSV(ghsa)GHSA-g4qm-m288-5cp9

Description

## Summary Coraza's cookie parser (`internal/cookies.ParseCookies`) does not strip ASCII control characters (CTLs) from the edges of a cookie name/value before they're matched against `REQUEST_COOKIES` / `REQUEST_COOKIES_NAMES`. When a CTL sits directly next to the `=` separator, Coraza absorbs it into the adjacent name or value, while several backend cookie parsers trim it away — so the WAF and the application disagree about the cookie it just received. ## Root cause - `ParseCookies` (`internal/cookies/cookies.go:17,26,31`) trims via `net/textproto.TrimString`, which strips only space (`0x20`) and tab (`0x09`). - RFC 6265 §4.1.1 defines cookie `name` as an HTTP `token` and `value` as `cookie-octet`, both excluding the full C0 control range (`0x00–0x1F`, `0x7F`) — not just space/tab. - Input `a\v=\t'` (vertical tab `\v` next to `=`) keeps `\v` in the name (`a\v`), yielding name=`a\v`, value=`\t'`. ## Confirmed divergence from real backends | Implementation | Name | Value | |---|---|---| | Coraza (< 3.8.0) | `a\v` | `\t'` | | Python `http.cookies`, and the Werkzeug/Flask version in the report below | `a` | `'` | | PHP `$_COOKIE` | `a` | `\t'` | | Node.js `cookie` package | `a\v` | `'` | RFC 6265 itself calls this exact cookie-pair invalid, so there's no single spec-correct reference — but Coraza's boundary handling diverges from 2 of these 3 widely-used backends. Correction (2026-10-02): current Werkzeug (3.1.9, checked during review of the 3.8.1 follow-up) keeps `a\v` as the name, like Node's `cookie` package. The name divergence therefore applies to PHP and to Python's `http.cookies`, not to every Werkzeug version. ## Impact An attacker can pad a `Cookie` header with a CTL adjacent to `=` so Coraza indexes a different name/value than the backend application does. A `SecRule` scoped to a specific cookie name or value can then miss the cookie the application actually processes — a WAF bypass for cookie-carried attacks. ## Affected component `internal/cookies.ParseCookies`, consumed via `REQUEST_COOKIES` / `REQUEST_COOKIES_NAMES`. ## Fix Trim the full CTL range (not just space/tab) from both ends of the extracted name and value, treating a boundary-adjacent CTL as a delimiter rather than token content — aligning with RFC 6265's `token`/`cookie-octet` grammar. The fix does not attempt to resolve what happens when a CTL lands in the *interior* of an otherwise-plausible name (e.g. `ab\vcd`). That case is disputed among the backends themselves — Python's `http.cookies` rejects the whole pair, PHP strips the CTL from the middle, Node's `cookie` package keeps it — so there is no consensus to converge on. It is left as a separate follow-up rather than guessed at here. ### Implementation note The trim is deliberately hand-rolled (a byte-wise scan on `b <= ' ' || b == 0x7f`, which covers octets `0x00–0x20` plus `0x7F`) rather than delegated to the standard library. This is a conscious choice on a security hot path and should not be "simplified" away later: - **`strings.TrimFunc` was measured and rejected.** It invokes its predicate through a func value once per byte scanned, which Go cannot devirtualize through `strings.indexFunc`. On an Apple M2, parsing a 64 KiB CTL-saturated `Cookie` header costs **167.6 µs** via `TrimFunc` versus **33.9 µs** byte-wise — a ~5× CPU amplification handed to an attacker, on input that is attacker-controlled and parsed on every request. Allocation counts are identical either way; the cost is purely the per-byte indirect call. - **`strings.TrimSpace` is not a substitute.** It misses most of the CTL range (`0x00–0x08`, `0x0E–0x1F`, `0x7F`) and additionally trims `U+0085` and `U+00A0`, whose multi-byte UTF-8 encodings a backend would not strip — reintroducing the very parser-disagreement class this advisory closes. The byte-wise implementation was verified equivalent to a `TrimFunc`-based one across all 16,843,009 byte strings of length 0–3, including invalid UTF-8, with zero mismatches. `BenchmarkParseCookies/CTLFlood` guards the hot path against a future regression to a per-byte indirect call. ## Proof of Concept (original report) > Hi, @fzipi, i hope you doing well, i'm RelunSec from InsiteTech.jp > > we discovered a parser confusion in cookie parser, i used a simple flask app that print the cookies > > ```py > from flask import Flask, request > > app = Flask(__name__) > > @app.route('/') > def index(): > # 1. Print all cookies as a dictionary to your terminal console > print("All cookies:", request.cookies) > > return "Cookies logged in terminal!" > > if __name__ == '__main__': > app.run(debug=True) > ``` > > and a go setup > > ```go > package cookies > > import ( > "fmt" > "testing" > ) > > func TestParseCookie(t *testing.T) { > inputs := []string{ > "a\v=\t'", > } > > fmt.Println("\n==========================================") > fmt.Println(" COOKIE PARSE DIRECT LOCAL RUN ") > fmt.Println("==========================================") > > for _, input := range inputs { > // Calling the exact lowercase function name from the repo > cookies := ParseCookies(input) > > fmt.Printf("-> Input: %q\n", input) > fmt.Printf(" Output: %q\n", cookies) > fmt.Println("------------------------------------------") > } > fmt.Println("==========================================") > } > ``` > > i runned the go program as you can see > > ```go > relunsec@relunsec:~/software/coraza/internal/cookies$ go test > > ========================================== > COOKIE PARSE DIRECT LOCAL RUN > ========================================== > -> Input: "a\v=\t'" > Output: map["a\v":["\t'"]] > ------------------------------------------ > ========================================== > PASS > ok github.com/corazawaf/coraza/v3/internal/cookies 0.003s > ``` > > and then i sended a curl request to the python flask web app > > ```bash > relunsec@relunsec:~/software/coraza/internal/cookies$ curl 127.0.0.1:5000 -H $'Cookie: a\v=\t' > Cookies logged in terminal! > ``` > > and then i saw in the running flask app terminal > > ```python > All cookies: ImmutableMultiDict([('a', "'")]) > ``` > > as you can see python see that as the a cookie and the value of it is `'`, while coraza see it in a different name and a value > > an attacker can craft a crafted payload that evade cookie inspection and then perfom their attack ### Patched in 3.8.1 The 3.8.0 fix was incomplete. 3.8.1 completes it: trimming control characters in 3.8.0 turned a cookie whose name is only control characters (`\x01=payload`) into a cookie with an empty name, and empty names have always been skipped, so its value was no longer inspected. Node's `cookie` package (`{"\x01": "payload"}`, `{"": "payload"}`) and Werkzeug still pass such pairs to the application. 3.8.1 keeps them in `REQUEST_COOKIES` under the name `""`. This is an intentional deviation from ModSecurity v2 and v3, which skip empty names. Upgrade to 3.8.1; 3.8.0 is listed as affected. ### 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). Unchanged vector; precondition stated per the project's triage guidance. Attack Complexity is High because the bypass depends on a specific backend cookie parser: the original trim discrepancy affects backends that split `a\v` as `a` (PHP's `$_COOKIE`, Python's `http.cookies`) and only rules keyed on a cookie name, and the 3.8.0 regression affects backends that pass empty or control-character-only cookie names to the application (Node's `cookie` package, Werkzeug for `\x01`). 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._

CVSS v3.1 base metrics

CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:N/I:L/A:N
—

Medium severity

Band computed from the CVSS base score, not the source's own label — so it means the same thing across every feed.

AV

Network

Attack Vector

AC

High

Attack Complexity

PR

None

Privileges Required

UI

None

User Interaction

S

Changed

Scope

C

None

Confidentiality

I

Low

Integrity

A

None

Availability

Affected

Vendor
Go
Product
github.com/corazawaf/coraza/v3

Versions

  • pkg:golang/github.com/corazawaf/coraza/v3 < 3.8.1

Stated as the source expressed them.