Vulnerability GHSA-g4qm-m288-5cp9

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:51 PM UTC
Coraza has Cookie Parser Confusion
v3.0.0-rc.1 - v3.8.0
v3.0.0-rc.1 - v3.8.0

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

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

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

Timeline

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