Cookie Max-Age Integer Overflow Vulnerability
What the report claims
The cookie parsing code in lib/cookie.c contains an integer overflow vulnerability when processing the Max-Age attribute of HTTP cookies, allowing attacker-controlled Max-Age values to cause incorrect cookie expiration calculations.
Compare with curl's recorded outcome
Labeled AI slop
curl's maintainer lists this report as AI slop
Shown for comparison after analysis. The pipeline did not see this label.
ProveIt's finding
The available evidence does not settle the report. A maintainer should investigate.
See the withdrawn model reasoning
The following reasoning failed the citation check and does not support the final finding.
- The overflow check `CURL_OFF_T_MAX - now < co->expires` is correct and cannot itself overflow because `now` (Unix timestamp, ~1.7e9) is orders of magnitude smaller than `CURL_OFF_T_MAX` (~9.2e18).
- The addition `co->expires += now` is executed only in the `else` branch where the check proved no overflow would occur.
- Parsed Max-Age values are capped at `CURL_OFF_T_MAX` by `curlx_str_number` (line 592, 596-599).
- After Max-Age processing, `cap_expires(now, co)` is called (line 614), which should enforce the RFC's 400-day cookie-age-limit.
- No integer overflow or wrap-around occurs in the provided code.
How this analysis ran 6 claims · 8 citations verified
Evidence check: judge quoted an external source that does not say that (https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-20.html)
Checked against curl master on 2026-01-19 (9703dabd77b5). Dev evaluation split. Citation verification checks that a referenced line was in the evidence; it does not prove the model interpreted it correctly.
Inspect the withdrawn draft reply
This draft belongs to the withdrawn judgment. It may contain unsupported claims.
Thank you for the detailed report. The code at lib/cookie.c:607-611 does not have the integer overflow you describe. The check `CURL_OFF_T_MAX - now < co->expires` is a standard safe addition guard: `now` comes from `time(NULL)` (~1.7e9) while `CURL_OFF_T_MAX` is ~9.2e18, so the subtraction cannot overflow. The addition only runs when the check proves it is safe. Parsed Max-Age values are capped at `CURL_OFF_T_MAX` by `curlx_str_number`, and `cap_expires` is called afterward to enforce the RFC's 400-day limit. No wrap-around or indefinite persistence occurs. If you have a concrete input that produces a wrong expiry with the current code, please share it.
Inspect all claims and source code 6 claims · 3 code excerpts
This model judgment was withdrawn. The claim assessments below are preserved for inspection and may be wrong.
Report claims
The overflow check CURL_OFF_T_MAX - now < co->expires can itself overflow when now is large, leading to incorrect bounds checking before adding now to co->expires
Read the reasoning
The report claims the subtraction `CURL_OFF_T_MAX - now` can overflow when `now` is large. In reality, `now` is the current Unix timestamp (seconds since 1970), which is at most a few billion even in the far future, while CURL_OFF_T_MAX is ~9.2 quintillion. The subtraction is completely safe.
The addition co->expires += now is performed without verifying that the result does not overflow, causing potential wrap‑around
Read the reasoning
The addition is guarded by the preceding overflow check. The code follows the standard safe pattern: check `MAX - b < a` before computing `a + b`.
The code uses CURL_OFF_T_MAX - now < co->expires as a guard, but this subtraction can underflow or overflow when now is close to CURL_OFF_T_MAX
Read the reasoning
The claim repeats C1's misunderstanding. `now` is a Unix timestamp, not an attacker-controlled value, and is always tiny compared to CURL_OFF_T_MAX.
An attacker can supply a Max-Age value that is extremely large (near CURL_OFF_T_MAX) to trigger the overflow conditions
Read the reasoning
An attacker can send a large Max-Age, but `curlx_str_number` caps the parsed value at CURL_OFF_T_MAX, and the overflow check at line 607 correctly handles that capped value. No overflow condition is triggered.
When the overflow occurs, co->expires may be set to CURL_OFF_T_MAX or wrap around, resulting in cookies that persist indefinitely or expire immediately
Read the reasoning
On overflow the code sets `co->expires` to CURL_OFF_T_MAX (saturating), not wrapping. Then `cap_expires` applies the RFC's 400-day limit. Cookies do not persist indefinitely or expire immediately due to this code path.
Multiple crafted Set-Cookie headers with large Max-Age values can fill the cookie jar, leading to denial of service
Read the reasoning
Cookie jar limits exist, but the claimed overflow does not occur (per C1-C5), so this DoS scenario does not follow from the alleged integer overflow. Whether a separate DoS exists via many cookies is not settled by the provided code.
What the report says about other projects
confirmed The RFC 6265bis draft specifies that Max-Age values must be non‑negative and that overflow must be handled safely
“If the expiry-time is more than cookie-age-limit, the user agent MUST set the expiry time to cookie-age-limit in seconds. If the expiry-time is earlier than the earliest date the user agent can represent, the user agent MAY replace the expiry-time with the earliest representable date.” source
Read the original report (excerpt)
Source code
| 428 | static CURLcode |
| 429 | parse_cookie_header(struct Curl_easy *data, |
| 430 | struct Cookie *co, |
| 431 | struct CookieInfo *ci, |
| 432 | bool *okay, /* if the cookie was fine */ |
| 433 | const char *ptr, |
| 434 | const char *domain, /* default domain */ |
| 435 | const char *path, /* full path used when this cookie is |
| 436 | set, used to get default path for |
| 437 | the cookie unless set */ |
| 438 | bool secure) /* TRUE if connection is over secure |
| 439 | origin */ |
| 440 | { |
| 441 | /* This line was read off an HTTP-header */ |
| 442 | time_t now = 0; |
| 443 | size_t linelength = strlen(ptr); |
| 444 | CURLcode result = CURLE_OK; |
| 445 | struct Curl_str cookie[COOKIE_PIECES]; |
| 446 | *okay = FALSE; |
| 447 | if(linelength > MAX_COOKIE_LINE) |
| C6: Line 447-449: lines longer than MAX_COOKIE_LINE are discarded. The cookie jar has other limits (MAX_NAME, etc.). | |
| 448 | /* discard overly long lines at once */ |
| 449 | return CURLE_OK; |
| 450 | |
| 451 | /* memset instead of initializer because gcc 4.8.1 is silly */ |
| 452 | memset(cookie, 0, sizeof(cookie)); |
| 453 | do { |
| 454 | struct Curl_str name; |
| 455 | struct Curl_str val; |
| 456 | |
| 457 | /* we have a <name>=<value> pair or a stand-alone word here */ |
| 458 | if(!curlx_str_cspn(&ptr, &name, ";\t\r\n=")) { |
| 459 | bool sep = FALSE; |
| 460 | curlx_str_trimblanks(&name); |
| 461 | |
| 462 | if(!curlx_str_single(&ptr, '=')) { |
| 463 | sep = TRUE; /* a '=' was used */ |
| 464 | if(!curlx_str_cspn(&ptr, &val, ";\r\n")) |
| 465 | curlx_str_trimblanks(&val); |
| 466 | } |
| 467 | else |
| 468 | curlx_str_init(&val); |
| 469 | |
| 470 | if(!curlx_strlen(&cookie[COOKIE_NAME])) { |
| 471 | /* The first name/value pair is the actual cookie name */ |
| 472 | if(!sep || |
| 473 | /* Bad name/value pair. */ |
| 474 | invalid_octets(curlx_str(&name), curlx_strlen(&name)) || |
| 475 | invalid_octets(curlx_str(&val), curlx_strlen(&val)) || |
| 476 | !curlx_strlen(&name)) { |
| 477 | infof(data, "invalid octets in name/value, cookie dropped"); |
| 478 | return CURLE_OK; |
| 479 | } |
| 480 | |
| 481 | /* |
| 482 | * Check for too long individual name or contents, or too long |
| 483 | * combination of name + contents. Chrome and Firefox support 4095 or |
| 484 | * 4096 bytes combo |
| 485 | */ |
| 486 | if(curlx_strlen(&name) >= (MAX_NAME - 1) || |
| 487 | curlx_strlen(&val) >= (MAX_NAME - 1) || |
| 488 | ((curlx_strlen(&name) + curlx_strlen(&val)) > MAX_NAME)) { |
| 489 | infof(data, "oversized cookie dropped, name/val %zu + %zu bytes", |
| 490 | curlx_strlen(&name), curlx_strlen(&val)); |
| 491 | return CURLE_OK; |
| 492 | } |
| 493 | |
| 494 | /* Reject cookies with a TAB inside the value */ |
| 495 | if(curlx_strlen(&val) && |
| 496 | memchr(curlx_str(&val), '\t', curlx_strlen(&val))) { |
| 497 | infof(data, "cookie contains TAB, dropping"); |
| 498 | return CURLE_OK; |
| 499 | } |
| 500 | |
| 501 | /* Check if we have a reserved prefix set. */ |
| 502 | if(!strncmp("__Secure-", curlx_str(&name), 9)) |
| 503 | co->prefix_secure = TRUE; |
| 504 | else if(!strncmp("__Host-", curlx_str(&name), 7)) |
| 505 | co->prefix_host = TRUE; |
| 506 | |
| 507 | cookie[COOKIE_NAME] = name; |
| 508 | cookie[COOKIE_VALUE] = val; |
| 509 | } |
| 510 | else if(!sep) { |
| 511 | /* |
| 512 | * this is a "<name>" with no content |
| 513 | */ |
| 514 | |
| 515 | /* |
| 516 | * secure cookies are only allowed to be set when the connection is |
| 517 | * using a secure protocol, or when the cookie is being set by |
| 518 | * reading from file |
| 519 | */ |
| 520 | if(curlx_str_casecompare(&name, "secure")) { |
| 521 | if(secure || !ci->running) |
| 522 | co->secure = TRUE; |
| 523 | else { |
| 524 | infof(data, "skipped cookie because not 'secure'"); |
| 525 | return CURLE_OK; |
| 526 | } |
| 527 | } |
| 528 | else if(curlx_str_casecompare(&name, "httponly")) |
| 529 | co->httponly = TRUE; |
| 530 | } |
| 531 | else if(curlx_str_casecompare(&name, "path")) { |
| 532 | cookie[COOKIE_PATH] = val; |
| 533 | } |
| 534 | else if(curlx_str_casecompare(&name, "domain") && curlx_strlen(&val)) { |
| 535 | bool is_ip; |
| 536 | const char *v = curlx_str(&val); |
| 537 | /* |
| 538 | * Now, we make sure that our host is within the given domain, or |
| 539 | * the given domain is not valid and thus cannot be set. |
| 540 | */ |
| 541 | |
| 542 | if('.' == *v) |
| 543 | curlx_str_nudge(&val, 1); |
| 544 | |
| 545 | #ifndef USE_LIBPSL |
| 546 | /* |
| 547 | * Without PSL we do not know when the incoming cookie is set on a |
| 548 | * TLD or otherwise "protected" suffix. To reduce risk, we require a |
| 549 | * dot OR the exact hostname being "localhost". |
| 550 | */ |
| 551 | if(bad_domain(curlx_str(&val), curlx_strlen(&val))) |
| 552 | domain = ":"; |
| 553 | #endif |
| 554 | |
| 555 | is_ip = Curl_host_is_ipnum(domain ? domain : curlx_str(&val)); |
| 556 | |
| 557 | if(!domain || |
| 558 | (is_ip && |
| 559 | !strncmp(curlx_str(&val), domain, curlx_strlen(&val)) && |
| 560 | (curlx_strlen(&val) == strlen(domain))) || |
| 561 | (!is_ip && cookie_tailmatch(curlx_str(&val), |
| 562 | curlx_strlen(&val), domain))) { |
| 563 | cookie[COOKIE_DOMAIN] = val; |
| 564 | if(!is_ip) |
| 565 | co->tailmatch = TRUE; /* we always do that if the domain name was |
| 566 | given */ |
| 567 | } |
| 568 | else { |
| 569 | /* |
| 570 | * We did not get a tailmatch and then the attempted set domain is |
| 571 | * not a domain to which the current host belongs. Mark as bad. |
| 572 | */ |
| 573 | infof(data, "skipped cookie with bad tailmatch domain: %s", |
| 574 | curlx_str(&val)); |
| 575 | return CURLE_OK; |
| 576 | } |
| 577 | } |
| 578 | else if(curlx_str_casecompare(&name, "max-age") && curlx_strlen(&val)) { |
| 579 | /* |
| 580 | * Defined in RFC2109: |
| 581 | * |
| 582 | * Optional. The Max-Age attribute defines the lifetime of the |
| 583 | * cookie, in seconds. The delta-seconds value is a decimal non- |
| 584 | * negative integer. After delta-seconds seconds elapse, the |
| 585 | * client should discard the cookie. A value of zero means the |
| 586 | * cookie should be discarded immediately. |
| 587 | */ |
| 588 | int rc; |
| 589 | const char *maxage = curlx_str(&val); |
| 590 | if(*maxage == '\"') |
| 591 | maxage++; |
| 592 | rc = curlx_str_number(&maxage, &co->expires, CURL_OFF_T_MAX); |
| C4: Line 592: `rc = curlx_str_number(&maxage, &co->expires, CURL_OFF_T_MAX);` passes CURL_OFF_T_MAX as the maximum. Lines 596-599: on STRE_OVERFLOW, `co->expires = CURL_OFF_T_MAX`. | |
| 593 | if(!now) |
| 594 | now = time(NULL); |
| 595 | switch(rc) { |
| 596 | case STRE_OVERFLOW: |
| 597 | /* overflow, used max value */ |
| 598 | co->expires = CURL_OFF_T_MAX; |
| 599 | break; |
| 600 | default: |
| 601 | /* negative or otherwise bad, expire */ |
| 602 | co->expires = 1; |
| 603 | break; |
| 604 | case STRE_OK: |
| 605 | if(!co->expires) |
| 606 | co->expires = 1; /* expire now */ |
| 607 | else if(CURL_OFF_T_MAX - now < co->expires) |
| C1: The check `CURL_OFF_T_MAX - now < co->expires` uses `now` from `time(NULL)` (line 594, 636). Current Unix time is ~1.7e9; CURL_OFF_T_MAX is 0x7FFFFFFFFFFFFFFF (~9.2e18). The subtraction cannot overflow or underflow because `now` is many orders of magnitude smaller than CURL_OFF_T_MAX. | |
| C2: Line 607 `else if(CURL_OFF_T_MAX - now < co->expires)` is the bounds check. If true (addition would overflow), line 609 sets `co->expires = CURL_OFF_T_MAX`. The addition `co->expires += now` at line 611 only executes in the `else` branch where the check proved no overflow. | |
| C3: Same as C1: `now` is never close to CURL_OFF_T_MAX. The subtraction cannot underflow (now is positive and far smaller than CURL_OFF_T_MAX) and cannot overflow (result fits in curl_off_t). | |
| C4: The subsequent check at line 607 correctly handles any value up to CURL_OFF_T_MAX. | |
| 608 | /* would overflow */ |
| 609 | co->expires = CURL_OFF_T_MAX; |
| C5: Line 609: `co->expires = CURL_OFF_T_MAX;` on detected overflow — no wrap-around. | |
| 610 | else |
| 611 | co->expires += now; |
| 612 | break; |
| 613 | } |
| 614 | cap_expires(now, co); |
| C5: Line 614: `cap_expires(now, co);` is called after Max-Age processing, which per RFC 6265bis should cap expiry to 400 days (cookie-age-limit). | |
| 615 | } |
| 616 | else if(curlx_str_casecompare(&name, "expires") && curlx_strlen(&val) && |
| 617 | !co->expires && (curlx_strlen(&val) < MAX_DATE_LENGTH)) { |
| 618 | /* |
| 619 | * Let max-age have priority. |
| 620 | * |
| 621 | * If the date cannot get parsed for whatever reason, the cookie |
| 622 | * will be treated as a session cookie |
| 623 | */ |
| 624 | char dbuf[MAX_DATE_LENGTH + 1]; |
| 625 | time_t date = 0; |
| 626 | memcpy(dbuf, curlx_str(&val), curlx_strlen(&val)); |
| 627 | dbuf[curlx_strlen(&val)] = 0; |
| 628 | if(!Curl_getdate_capped(dbuf, &date)) { |
| 629 | if(!date) |
| 630 | date++; |
| 631 | co->expires = (curl_off_t)date; |
| 632 | } |
| 633 | else |
| 634 | co->expires = 0; |
| 635 | if(!now) |
| 636 | now = time(NULL); |
| 569 | # define CURL_OFF_T_MAX 0x7FFFFFFFFFFFFFFF |
Show 4 lines (not cited by the judge)
| 195 | int curlx_str_number(const char **linep, curl_off_t *nump, curl_off_t max) |
| 196 | { |
| 197 | return str_num_base(linep, nump, max, 10); |
| 198 | } |