Vulnerability GHSA-g4qm-m288-5cp9
Summary
Coraza has Cookie Parser Confusion
Details
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 vianet/textproto.TrimString, which strips only space (0x20) and tab (0x09).- RFC 6265 §4.1.1 defines cookie
nameas an HTTPtokenandvalueascookie-octet, both excluding the full C0 control range (0x00–0x1F,0x7F) — not just space/tab. - Input
a\v=\t'(vertical tab\vnext to=) keeps\vin 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.TrimFuncwas measured and rejected. It invokes its predicate through a func value once per byte scanned, which Go cannot devirtualize throughstrings.indexFunc. On an Apple M2, parsing a 64 KiB CTL-saturatedCookieheader costs 167.6 µs viaTrimFuncversus 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.TrimSpaceis not a substitute. It misses most of the CTL range (0x00–0x08,0x0E–0x1F,0x7F) and additionally trimsU+0085andU+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
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
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
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.003sand then i sended a curl request to the python flask web app
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
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 valuean 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.
Related Vulnerabilities
Other vulnerabilities affecting the same packages