✓ProveIt
Examples / Report #3972395 Saved analysis

CVE-2026-80255: secure cookie attribute bypass with tab

curl · 2026-08-2615.3s analysis · $0.021 in model calls

What the report claims

A Set-Cookie header using HTAB before the Secure attribute causes curl to store the cookie without its Secure flag, leading to plaintext transmission of the cookie over subsequent HTTP requests.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-80255 secure cookie attribute bypass with tab

Shown for comparison after analysis. The pipeline did not see this label.

ProveIt's finding

Code supports the claim

The code supports the reported flaw. A maintainer should review its impact and decide what to do next.

Why this finding?
  • The provided code at lib/cookie.c:610-656 shows a do/while loop that consumes ';' but does not skip blank characters (space or HTAB) before parsing the next attribute name.
  • curlx_str_cspn at line 614 rejects HTAB in its reject set, so a leading HTAB causes the attribute parse to fail silently.
  • The Secure flag parsing (parse_flag at line 642) is inside the skipped if block, so the cookie is stored without the Secure flag.
  • The cookie is then sent over plaintext HTTP, violating the confidentiality boundary the Secure attribute establishes.
  • The fix is straightforward: add curlx_str_passblanks(&ptr) after the ';' is consumed.
  • The external RFC 6265 claim about HTAB as valid whitespace is not explicitly confirmed by the provided excerpt, but curl's own ISBLANK macro treats HTAB as blank, indicating the project considers it valid whitespace.
How this analysis ran 5 claims · 15 citations verified

Checked against curl tested commit c2676bf9e639 (c2676bf9e639). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.

1 · Grounding
grounded
no model · 14 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2337 tokens · $0.0003
3 · Evidence
6 code snippets
real curl source at c2676bf9e639 · 6 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
15029 tokens · 13.2s · $0.0211
5 · Receipts
15 of 15 citations verified
every citation must point at code the judge was shown
Review a draft reply to the reporter

Edit and verify before sending.

Thank you for the detailed report. The code confirms the flaw: parse_cookie_header() in lib/cookie.c does not skip HTAB after consuming ';', causing attributes starting with HTAB to be silently dropped. The Secure flag is lost and the cookie leaks over HTTP. We will add curlx_str_passblanks() after the ';' consumption and a regression test. The fix is targeted for the next release.

Inspect all claims and source code 5 claims · 6 code excerpts

Report claims

C1flaw supported

parse_cookie_header() fails to parse attributes that start with HTAB, causing the Secure flag to be silently dropped

Read the reasoning

The do/while loop at lines 610-656 parses each attribute. After consuming ';', if the next character is HTAB, curlx_str_cspn at line 614 returns failure (STRE_SHORT) because HTAB is in its reject set. The if block (lines 615-655) containing parse_flag for 'secure' is skipped. Then curlx_str_single at line 656 fails to match ';', exiting the loop. The cookie is stored at lines 658-662 without the Secure flag.

C2location supported

In lib/cookie.c, parse_cookie_header() uses a do/while loop that does not skip HTAB after consuming ';'

Read the reasoning

The loop at lines 610-656 has no call to curlx_str_passblanks or equivalent blank-skipping after consuming ';' at line 656 and before the next curlx_str_cspn at line 614. The helper curlx_str_passblanks exists (strparse.c:313-317) and uses ISBLANK which includes HTAB (curl_ctype.h:43), but it is not invoked here.

C3trigger supported

A Set-Cookie header containing a literal HTAB (0x09) before the Secure attribute, e.g. 'Set-Cookie: sid=SECRET;\tSecure'

Read the reasoning

A Set-Cookie header like 'sid=SECRET;\tSecure' leaves ptr at HTAB after the ';' is consumed. The next curlx_str_cspn call sees HTAB as the first character, which is in its reject set, so it returns failure and the attribute 'Secure' is never parsed.

C4impact supported

The cookie is stored without the Secure flag and is subsequently sent over plaintext HTTP on follow‑up requests to the same host

Read the reasoning

Since the Secure attribute is never parsed (parse_flag not called), the cookie is stored without the Secure flag. Per curl's cookie storage model, cookies without the Secure flag are sent over both HTTPS and HTTP, causing the secret to leak on plaintext connections.

C5reproduction supported

Steps 1‑8 in the report reproduce the bug by requesting /htab, saving the cookie jar, and then making an HTTP request that echoes the cookie

Read the reasoning

The reproduction steps exercise the exact code path: HTTPS request sets cookie with HTAB before Secure, cookie jar saves it without Secure flag, subsequent HTTP request sends the cookie. The provided code confirms this path exists.

What the report says about other projects

not addressed HTAB is valid optional whitespace per RFC 6265 section 5.2 and the HTTP grammar (OWS)

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary A `Set-Cookie` header using HTAB (horizontal tab) instead of SPACE before the `Secure` attribute — e.g. `Set-Cookie: sid=SECRET;\tSecure` — causes curl to store the cookie without its Secure flag. The cookie is then sent over plaintext HTTP on subsequent requests to the same host. The space-separated form `Set-Cookie: sid=SECRET; Secure` works correctly. ## Affected versions curl 8.13.0 through at least 8.21.0 (all versions containing commit `1aea05a6c26`, "cookie: convert to using strparse", 2025-02-18). ## Root cause In `lib/cookie.c`, `parse_cookie_header()` uses a do/while loop that parses each cookie attribute: ```c do { if(!curlx_str_cspn(&ptr, &name, ";\t\r\n=")) { // parse attribute } } while(!curlx_str_single(&ptr, ';')); ``` After consuming the `;` separator, `ptr` points at the HTAB. Since `\t` is in the reject set of `curlx_str_cspn`, the function sees it as the first character, returns an empty match (failure), and the `if` block is skipped. Then `curlx_str_single(&ptr, ';')` fails because the current byte is `\t`, not `;`, so the loop exits. The `Secure` attribute is never parsed, but the cookie is still stored. The previous parser (before `1aea05a6c26`) had `while(ISBLANK(*ptr)) ptr++;` before parsing each attribute, which correctly skipped HTAB. This blank-skipping was lost in the strparse conversion. The helper `curlx_str_passblanks()` exists in the codebase but is not called here. ## Reproduction 1. Build curl at commit c2676bf9e639e31e44b12a2cc82cf2046231b056 (or any version from 8.13.0 onward) with OpenSSL support enabled. 2. Generate a self-signed TLS certificate for the hostname test.invalid, valid for at least one day. 3. Start a local server that listens on two ports: HTTPS on port 8443 — serves Set-Cookie: sid=HTAB_SECRET;\tSecure (with a literal HTAB character 0x09 before Secure) when the path is /htab, and Set-Cookie: sid=SPACE_SECRET; Secure (with a normal space) when the path is /space. HTTP on port 8080 — echoes back the received Cookie header value in the response body. 4. Set the HTAB cookie over HTTPS by requesting https://test.invalid:8443/htab, saving the cookie jar: ``` curl -ks --noproxy '*' \ --resolve test.invalid:8443:127.0.0.1 \ -c /tmp/htab.jar \ https://test.invalid:8443/htab Make a follow-up request over plaintext HTTP using the saved cookie jar: ``` ``` curl -sS --nop …

Source code

lib/curlx/strparse.clines 283–299 · definition of curlx_str_cspn
283int curlx_str_cspn(const char **linep, struct Curl_str *out,
284 const char *reject)
285{
286 const char *s = *linep;
287 size_t len;
288 DEBUGASSERT(linep && *linep);
289
290 len = strcspn(s, reject);
C3: strcspn returns 0 length when first character is in reject set, causing STRE_SHORT
291 if(len) {
292 out->str = s;
293 out->len = len;
294 *linep = &s[len];
295 return STRE_OK;
296 }
297 curlx_str_init(out);
298 return STRE_SHORT;
299}
lib/curl_ctype.hlines 43–43 · definition of ISBLANK
43#define ISBLANK(x) (((x) == ' ') || ((x) == '\t'))
C2: ISBLANK macro defines blank as space or HTAB
lib/curlx/strparse.clines 313–317 · definition of curlx_str_passblanks
313void curlx_str_passblanks(const char **linep)
C2: curlx_str_passblanks exists and skips ISBLANK (space or HTAB) but is not called in parse_cookie_header
314{
315 while(ISBLANK(**linep))
316 (*linep)++; /* move over it */
317}
lib/cookie.clines 587–665 · definition of parse_cookie_header
C5: parse_cookie_header is the entry point for Set-Cookie header parsing
C2: do/while loop starts here with no blank-skipping before curlx_str_cspn
C1: curlx_str_cspn rejects HTAB in its reject set ";\t\r\n=", so an attribute starting with HTAB fails to parse
C3: curlx_str_cspn called with reject set including HTAB; a leading HTAB causes immediate failure
C1: parse_flag for Secure attribute is inside the if block that is skipped when curlx_str_cspn fails
C4: parse_flag for 'secure' is only called inside the if block that is skipped
C1: curlx_str_single(&ptr, ';') fails because current byte is HTAB, not ';', causing loop exit
C2: Loop condition consumes ';' but does not skip blanks before next iteration
C1: Cookie is still stored via storecookie() even though Secure flag was never parsed
C4: Cookie is stored if COOKIE_NAME has length, regardless of whether Secure flag was parsed
C4: storecookie called with cookie data that lacks Secure flag
C5: storecookie persists the cookie to the cookie jar
lib/curlx/strparse.clines 138–145 · definition of curlx_str_single
Show 8 lines (not cited by the judge)
138int curlx_str_single(const char **linep, char byte)
139{
140 DEBUGASSERT(linep && *linep);
141 if(**linep != byte)
142 return STRE_BYTE;
143 (*linep)++; /* move over it */
144 return STRE_OK;
145}
lib/optiontable.pllines 84–125 · definition of while
Show 42 lines (not cited by the judge)
84while(<CURL_H>) {
85 my $l = $_;
86 if($fl) {
87 # continued deprecation
88 if($l =~ /(.*)\),/) {
89 $fl .= $1;
90
91 # the end
92 my @p = split(/, */, $fl);
93 add($p[0], $p[1], $p[2]);
94 undef $fl;
95 }
96 else {
97 # another line to append
98 chomp $l;
99 $fl .= $l;
100 }
101 }
102
103 if(/^ *CURLOPTDEPRECATED\((.*)/) {
104 $fl = $1;
105 chomp $fl;
106 }
107
108 if(/^ *CURLOPT\(([^,]*), ([^,]*), (\d+)\)/) {
109 my($opt, $type, $num) = ($1, $2, $3);
110 add($opt, $type, $num);
111 }
112
113 # alias for an older option
114 # old = new
115 if(/^#define (CURLOPT_[^ ]*) *(CURLOPT_\S*)/) {
116 my ($o, $n) = ($1, $2);
117 # skip obsolete ones
118 if(($n !~ /OBSOLETE/) && ($o !~ /OBSOLETE/)) {
119 $o =~ s/^CURLOPT_//;
120 $n =~ s/^CURLOPT_//;
121 $alias{$o} = $n;
122 push @names, $o;
123 }
124 }
125}