---
id: GHSA-g4qm-m288-5cp9
title: Coraza has Cookie Parser Confusion
summary: Coraza has Cookie Parser Confusion
severity: medium
cvss: 4
cwe:
  - CWE-436
vendor: corazawaf
product: github.com/corazawaf/coraza/v3
ecosystem: go
affected:
  - github.com/corazawaf/coraza/v3 < 3.8.1
patched:
  - github.com/corazawaf/coraza/v3 3.8.1
published: '2026-10-08'
updated: '2026-10-08'
sourceUpdated: '2026-10-08T17:51:32Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-g4qm-m288-5cp9'
references:
  - url: >-
      https://github.com/corazawaf/coraza/security/advisories/GHSA-g4qm-m288-5cp9
  - url: >-
      https://github.com/corazawaf/coraza/commit/0b940e197ad9983fb3aa36e84f1f81ff985461af
  - url: >-
      https://github.com/corazawaf/coraza/commit/9f8521398d1ff023b958fad0b944cac265763866
  - url: 'https://github.com/corazawaf/coraza/releases/tag/v3.8.1'
  - url: 'https://github.com/advisories/GHSA-g4qm-m288-5cp9'
tags:
  - ghsa
  - go
ingestedAt: '2026-10-08T17:56:11.722Z'
---

## Overview

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

## Affected packages

- `github.com/corazawaf/coraza/v3 < 3.8.1`

## Remediation

Upgrade to a patched release:

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