✓ProveIt
Examples / Report #4021979 Saved analysis

CVE-2026-92386: server-set cookie aborts cookie jar load

curl · 2026-09-1117.2s analysis · $0.025 in model calls

What the report claims

A remote server can poison a shared cookie jar with an oversized Set-Cookie header that causes curl to fail when reloading the jar, breaking subsequent transfers.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-92386 server-set cookie aborts cookie jar load

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?
  • All five claims are directly supported by the provided source code at the reported revision.
  • The inconsistency between the 5000-byte acceptance limit (excluding NUL) and the 5000-byte reload limit (including NUL) is real and exploitable.
  • A remote server can send a Set-Cookie header that passes both acceptance checks but serializes to >5000 bytes, which the loader then rejects with CURLE_TOO_LARGE.
  • The error occurs in Curl_pretransfer before any network I/O, and because the jar is shared and sorted newest-first, one malicious origin persistently breaks the jar for all unrelated origins — a cross-origin persistent denial of service.
  • This is not listed in the project's 'not security issues' sections; it is a server-triggered, persistent DoS affecting shared state across origins.
How this analysis ran 5 claims · 14 citations verified

Checked against curl tested commit e5e1c356107e (e5e1c356107e). 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
partially grounded
no model · 14 references checked out, 2 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
5 claims · 2981 tokens · $0.0003
3 · Evidence
10 code snippets
real curl source at e5e1c356107e · 1 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
17685 tokens · 14.2s · $0.0249
5 · Receipts
14 of 14 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 and PoC. The code confirms the inconsistency: parse_cookie_header and parse_first_pair accept a cookie that get_netscape_format serializes beyond the loader's MAX_COOKIE_LINE bound (which includes the NUL terminator). The oversized line is written to the jar, sorted first, and causes CURLE_TOO_LARGE on reload in Curl_pretransfer — a persistent cross-origin DoS. We'll treat this as a security issue and work on a fix that aligns the size contract across acceptance, serialization, and loading.

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

Report claims

C1flaw supported

parse_cookie_header() accepts a Set-Cookie line up to MAX_COOKIE_LINE (5000) bytes, and parse_first_pair() permits a name and value totaling 4096 bytes, allowing a value to be crafted that exactly hits both limits.

Read the reasoning

Both limits exist and are independent: parse_cookie_header enforces 5000-byte line limit, parse_first_pair enforces 4096-byte name+value limit. A crafted cookie can satisfy both simultaneously.

C2flaw supported

get_netscape_format() serializes cookies without checking the resulting length, producing at least 5013 physical bytes for the crafted cookie, which exceeds the 5000-byte reload limit.

Read the reasoning

The serialization adds fixed overhead (6 tabs, 'TRUE'/'FALSE', expires number, domain prefix, httponly prefix) to the already-maximal name+value+path. The example cookie (1+4095 name/value, ~893 path, domain, etc.) exceeds 5000 physical bytes; the report's 5013 is plausible and the code has no guard against it.

C3flaw supported

Curl_get_line() uses dyn_nappend() which counts the terminating NUL against the MAX_COOKIE_LINE bound, rejecting any jar line at or beyond 5000 bytes and returning CURLE_TOO_LARGE.

Read the reasoning

The loader's dynbuf is capped at 5000 including the NUL terminator, so any line of 5000 or more physical bytes (i.e., 4999 content bytes + NUL) is rejected with CURLE_TOO_LARGE. This is stricter than the 5000-byte acceptance limit.

C4flaw supported

The oversized cookie can be written to the jar file and end up first due to newest-first sorting, making the entire jar unloadable on subsequent requests.

Read the reasoning

cookie_output writes all cookies sorted by creation time (newest first per report). The oversized cookie, having the newest timestamp, will be written first in the jar. The code shows no length check before writing.

C5flaw supported

The reload path aborts before any network I/O, causing a denial of service for unrelated transfers that use the same cookie jar.

Read the reasoning

Cookie loading occurs in Curl_pretransfer before any network I/O. An oversized line causes CURLE_TOO_LARGE, which bubbles up and aborts the transfer. Since the jar is shared, one poisoned entry breaks all subsequent transfers using that jar.

Deterministic checks that failed

  • symbol Curl_dyn_init exists in other curl releases but not at e5e1c356107e
  • quote only 1/3 quoted lines appear anywhere in curl at e5e1c356107e

What the report says about other projects

confirmed MAX_COOKIE_LINE is 5000; name+value may not exceed 4096 bytes

“Max 4095 bytes length per cookie + name and value. Name + value may not exceed 4096 bytes. + */ #define MAX_COOKIE_LINE 5000” source

Read the original report (excerpt)

Full report on HackerOne ↗

Hi all, I found a way for a remote server to poison a shared cookie jar so that later, unrelated transfers fail before any network I/O happens. It just has to send a Set-Cookie that curl happily accepts but can never read back from its own jar file. ## The bug `parse_cookie_header()` accepts a Set-Cookie line up to `MAX_COOKIE_LINE` (5000) bytes. `parse_first_pair()` separately permits a name and value totaling 4096 bytes. Those two limits are independent, so a value can be crafted to land right at the edge of both. For example: on `https://a.co/`, `n=` followed by a 4095-byte value and `; Path=/` followed by 895 more path bytes comes to exactly 5000 bytes total, exactly 4096 for name+value, with a valid 896-byte path. It sails through both checks. The problem shows up on the write side. `get_netscape_format()` serializes domain, six tabs, tailmatch, path, secure flag, expiry, name, and value with no check on the resulting total length. That example alone produces at least 5013 physical bytes on disk including the newline — already past the 5000-byte ceiling the *reload* path enforces. On reload, the line dynbuf is initialized with `MAX_COOKIE_LINE`, and `Curl_get_line()` calls `dyn_nappend()`: ```c // lib/cookie.c:1184-1186 struct dynbuf buf; Curl_dyn_init(&buf, MAX_COOKIE_LINE); while(Curl_get_line(&buf, fp)) { ``` `dyn_nappend()` counts the terminating NUL against that bound, so it accepts at most 4999 physical bytes and returns `CURLE_TOO_LARGE` for any jar line at or beyond that. A record that was perfectly valid to accept and write is not valid to read back: the same limit gets applied inconsistently on the two sides of a round trip. Cookie loading stops dead at that error, and `Curl_pretransfer()` propagates it before any network I/O starts. It gets worse: newly received cookies get increasing creation times, and `cookie_output()` sorts newest-first. So the attacker's oversized record can end up ahead of older, legitimate entries in the file. As soon as it's written, one origin has poisoned a jar shared across unrelated destinations: every subsequent transfer that loads that jar fails immediately, until someone finds and removes the bad line or deletes the file. ## PoC Tested against the audit revision `e5e1c356107ea4f8208757deb69776d94496cfae`. ```sh #!/usr/bin/env bash set -eu CURL_BIN=${CURL_BIN:-"$(pwd)/build-poc/src/curl"} test -x " …

Source code

lib/cookie.clines 1159–1211 · lines 1184-1186 cited by the report
C3: curlx_dyn_init(&buf, MAX_COOKIE_LINE) — buffer bound is 5000
C5: Curl_get_line returns CURLE_TOO_LARGE on oversized line, aborting the load loop
lib/cookie.hlines 81–81 · definition of MAX_COOKIE_LINE
C1: MAX_COOKIE_LINE defined as 5000
lib/cookie.clines 587–665 · definition of parse_cookie_header
C1: parse_cookie_header rejects lines longer than MAX_COOKIE_LINE
lib/cookie.clines 418–449 · definition of parse_first_pair
C1: parse_first_pair rejects if name+value length > MAX_NAME (4096 per external source and comment)
lib/cookie.clines 1494–1518 · definition of get_netscape_format
C2: get_netscape_format definition
C2: curl_maprintf with format including domain, tailmatch, path, secure, expires, name, value — no length check
lib/curl_get_line.clines 35–67 · definition of Curl_get_line
35CURLcode Curl_get_line(struct dynbuf *buf, FILE *input, bool *eof)
36{
37 CURLcode result;
38 char buffer[128];
39 curlx_dyn_reset(buf);
40 while(1) {
41 size_t rlen;
42 const char *b = fgets(buffer, sizeof(buffer), input);
43 if(!b && ferror(input))
44 return CURLE_READ_ERROR;
45
46 *eof = feof(input);
47
48 rlen = b ? strlen(b) : 0;
49 if(rlen) {
50 result = curlx_dyn_addn(buf, b, rlen);
C3: curlx_dyn_addn(buf, b, rlen) appends line data
51 if(result)
52 /* too long line or out of memory */
53 return result;
54 }
55 /* now check the full line */
56 rlen = curlx_dyn_len(buf);
57 b = curlx_dyn_ptr(buf);
58 if(rlen && (b[rlen - 1] == '\n'))
59 /* LF at end of the line */
60 return CURLE_OK; /* all good */
61 if(*eof)
62 /* append a newline */
63 return curlx_dyn_addn(buf, "\n", 1);
64 /* otherwise get next line to append */
65 }
66 /* UNREACHABLE */
67}
lib/curlx/dynbuf.clines 67–119 · definition of dyn_nappend
67static CURLcode dyn_nappend(struct dynbuf *s,
68 const unsigned char *mem, size_t len)
69{
70 size_t idx = s->leng;
71 size_t a = s->allc;
72 size_t fit = len + idx + 1; /* new string + old string + zero byte */
C3: fit = len + idx + 1 includes terminating NUL
73
74 /* try to detect if there is rubbish in the struct */
75 DEBUGASSERT(s->init == DYNINIT);
76 DEBUGASSERT(s->toobig);
77 DEBUGASSERT(idx < s->toobig);
78 DEBUGASSERT(!s->leng || s->bufr);
79 DEBUGASSERT(a <= s->toobig);
80 DEBUGASSERT(!len || mem);
81
82 if(fit > s->toobig) {
C3: if fit > s->toobig (5000) returns CURLE_TOO_LARGE
83 curlx_dyn_free(s);
84 return CURLE_TOO_LARGE;
85 }
86 else if(!a) {
87 DEBUGASSERT(!idx);
88 /* first invoke */
89 if(MIN_FIRST_ALLOC > s->toobig)
90 a = s->toobig;
91 else if(fit < MIN_FIRST_ALLOC)
92 a = MIN_FIRST_ALLOC;
93 else
94 a = fit;
95 }
96 else {
97 while(a < fit)
98 a *= 2;
99 if(a > s->toobig)
100 /* no point in allocating a larger buffer than this is allowed to use */
101 a = s->toobig;
102 }
103
104 if(a != s->allc) {
105 void *p = curlx_realloc(s->bufr, a);
106 if(!p) {
107 curlx_dyn_free(s);
108 return CURLE_OUT_OF_MEMORY;
109 }
110 s->bufr = p;
111 s->allc = a;
112 }
113
114 if(len)
115 memcpy(&s->bufr[idx], mem, len);
116 s->leng = idx + len;
117 s->bufr[s->leng] = 0;
118 return CURLE_OK;
119}
lib/cookie.clines 1528–1623 · definition of cookie_output
C4: qsort with cookie_sort_ct — report states newest-first ordering
C4: curl_mfprintf writes each serialized cookie via get_netscape_format
lib/transfer.clines 440–590 · definition of Curl_pretransfer
440CURLcode Curl_pretransfer(struct Curl_easy *data)
441{
442 CURLcode result = CURLE_OK;
443
444 /* Reset the retry count at the start of each request.
445 * If the retry count is not reset, when the connection drops,
446 * it will not enter the retry mechanism on CONN_MAX_RETRIES + 1 attempts
447 * and will immediately throw
448 * "Connection died, tried CONN_MAX_RETRIES times before giving up".
449 * By resetting it here, we ensure each new request starts fresh. */
450 data->state.retrycount = 0;
451
452 if(!CURL_EASY_STR(data, STRING_SET_URL) && !data->set.uh) {
453 /* we cannot do anything without URL */
454 failf(data, "No URL set");
455 return CURLE_URL_MALFORMAT;
456 }
457
458 /* CURLOPT_CURLU overrides CURLOPT_URL and the contents of the CURLU handle
459 is allowed to be changed by the user between transfers */
460 if(data->set.uh) {
461 char *url = NULL;
462 CURLUcode uc;
463 uc = curl_url_get(data->set.uh, CURLUPART_URL, &url, 0);
464 if(uc) {
465 /* clear the pointer to not point to freed memory anymore */
466 Curl_bufref_set(&data->state.url, NULL, 0, NULL);
467 failf(data, "No URL set");
468 return CURLE_URL_MALFORMAT;
469 }
470 result = CURL_EASY_STR_SETN(data, STRING_SET_URL, url);
471 if(result)
472 return result;
473 }
474
475 Curl_bufref_set(&data->state.url, CURL_EASY_STR(data, STRING_SET_URL),
476 0, NULL);
477
478 if(data->set.postfields && data->set.set_resume_from) {
479 /* we cannot */
480 failf(data, "cannot mix POSTFIELDS with RESUME_FROM");
481 return CURLE_BAD_FUNCTION_ARGUMENT;
482 }
483
484 data->state.prefer_ascii = data->set.prefer_ascii;
485#ifdef CURL_LIST_ONLY_PROTOCOL
486 data->state.list_only = data->set.list_only;
487#endif
488 data->state.httpreq = data->set.method;
489
490 /* initial transfer request coming up, forget the initial origin
491 * from a previous perform() on this handle. */
492 Curl_peer_unlink(&data->state.initial_origin);
493 Curl_peer_unlink(&data->state.origin);
494 data->state.requests = 0;
495 data->state.followlocation = 0; /* reset the location-follow counter */
496 data->state.this_is_a_follow = FALSE; /* reset this */
497 data->state.http_ignorecustom = FALSE; /* use custom HTTP method */
498 data->state.errorbuf = FALSE; /* no error has occurred */
499#ifndef CURL_DISABLE_HTTP
500 Curl_http_neg_init(data, &data->state.http_neg);
501#endif
502 data->state.authproblem = FALSE;
503 data->state.authhost.want = data->set.httpauth;
504 data->state.authproxy.want = data->set.proxyauth;
505 curlx_safefree(data->info.wouldredirect);
506 Curl_data_priority_clear_state(data);
507 if(data->set.http_auto_referer)
508 Curl_bufref_free(&data->state.referer);
509 if(CURL_EASY_STR(data, STRING_SET_REFERER))
510 Curl_bufref_set(&data->state.referer,
511 CURL_EASY_STR(data, STRING_SET_REFERER), 0, NULL);
512 else
513 Curl_bufref_free(&data->state.referer);
514
515 if(data->state.httpreq == HTTPREQ_PUT)
516 data->state.infilesize = data->set.filesize;
517 else if((data->state.httpreq != HTTPREQ_GET) &&
518 (data->state.httpreq != HTTPREQ_HEAD)) {
519 data->state.infilesize = data->set.postfieldsize;
520 if(data->set.postfields && (data->state.infilesize == -1))
521 data->state.infilesize = (curl_off_t)strlen(data->set.postfields);
522 }
523 else
524 data->state.infilesize = 0;
525
526 /* If there is a list of cookie files to read, do it now! */
527 result = Curl_cookie_loadfiles(data,
C5: Curl_cookie_loadfiles called in Curl_pretransfer
528 data->set.cookiesession ?
529 COOKIE_NOSESSION : 0);
530 if(!result)
C5: if result is error, Curl_cookie_run skipped and error propagates
531 Curl_cookie_run(data); /* activate */
532
533 /* If there is a list of host pairs to deal with */
534 if(!result && data->state.resolve)
535 result = Curl_loadhostpairs(data);
536
537 if(!result)
538 /* If there is a list of hsts files to read */
539 result = Curl_hsts_loadfiles(data);
540
541 if(!result) {
542 /* Allow data->set.use_port to set which port to use. This needs to be
543 * disabled for example when we follow Location: headers to URLs using
544 * different ports! */
545 data->state.allow_port = TRUE;
546
547#if defined(HAVE_SIGNAL) && defined(SIGPIPE) && !defined(MSG_NOSIGNAL)
548 /*************************************************************
549 * Tell signal handler to ignore SIGPIPE
550 *************************************************************/
551 if(!data->set.no_signal)
552 data->state.prev_signal = signal(SIGPIPE, SIG_IGN);
553#endif
554
555 Curl_initinfo(data); /* reset session-specific information "variables" */
556 Curl_pgrsResetTransferSizes(data);
557 Curl_pgrsStart(data, NULL);
558
559 /* In case the handle is reused and an authentication method was picked
560 in the session we need to make sure we only use the one(s) we now
561 consider to be fine */
562 data->state.authhost.picked &= data->state.authhost.want;
563 data->state.authproxy.picked &= data->state.authproxy.want;
564
565#ifndef CURL_DISABLE_FTP
566 data->state.wildcardmatch = data->set.wildcard_enabled;
567 if(data->state.wildcardmatch) {
568 struct WildcardData *wc;
569 if(!data->wildcard) {
570 data->wildcard = curlx_calloc(1, sizeof(struct WildcardData));
571 if(!data->wildcard)
572 return CURLE_OUT_OF_MEMORY;
573 }
574 wc = data->wildcard;
575 if(wc->state < CURLWC_INIT) {
576 if(wc->ftpwc)
577 wc->dtor(wc->ftpwc);
578 curlx_safefree(wc->pattern);
579 curlx_safefree(wc->path);
580 Curl_wildcard_init(wc); /* init wildcard structures */
581 }
582 }
583#endif
584 result = Curl_hsts_loadcb(data, data->hsts);
585 }
586
587 data->req.headerbytecount = 0;
588 Curl_headers_cleanup(data);
589 return result;
590}
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}