✓ProveIt
Examples / Report #3671818 Saved analysis

CVE-2026-6276: stale custom cookie host causes cookie leak

curl · 2026-04-1414.5s analysis · $0.018 in model calls

What the report claims

A stale custom cookie host causes cookie leak by reusing stale cookiehost value across requests on the same easy handle.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-6276 stale custom cookie host causes cookie leak

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 code at lib/http.c:2065 sets data->state.aptr.cookiehost from a custom Host: header.
  • The non-custom-host path (lib/http.c:2075-2089) does not clear data->state.aptr.cookiehost.
  • Outgoing cookie selection (lib/http.c:2560-2561) and incoming Set-Cookie attribution (lib/http.c:3561-3562) both prefer data->state.aptr.cookiehost over the actual connection host.
  • This allows cross-origin cookie leakage and injection across reused easy handles, a confidentiality and integrity breach.
  • The issue is present in the checked revision (master on 2026-04-14) and matches the reported version curl 8.19.0.
How this analysis ran 6 claims · 9 citations verified

Checked against curl master on 2026-04-14 (bcb02fe5265f). Test 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 · 9 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
6 claims · 1972 tokens · $0.0002
3 · Evidence
3 code snippets
real curl source at bcb02fe5265f · 2 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
11392 tokens · 11.6s · $0.0176
5 · Receipts
9 of 9 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 flaw: lib/http.c:2065 sets cookiehost from a custom Host: header, but the non-custom-host path at lines 2075-2089 does not clear it. Both outgoing cookie selection (line 2560) and Set-Cookie attribution (line 3561) then reuse the stale value, enabling cross-origin cookie leakage and injection. This is a valid security issue in curl 8.19.0 and current master. We will prepare a fix to clear cookiehost when switching away from a custom Host: header.

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

Report claims

C1flaw supported

libcurl keeps a stale data->state.aptr.cookiehost after a request that uses a custom Host: header

Read the reasoning

The code at line 2065 assigns the extracted custom Host: hostname to aptr->cookiehost (data->state.aptr.cookiehost) when a custom Host: header is present and conditions are met.

C2flaw supported

On later requests without a custom Host: header, libcurl reuses that stale value for outgoing cookie selection

Read the reasoning

http_cookies() at line 2560-2561 uses data->state.aptr.cookiehost if set, otherwise falls back to the actual connection host. Since cookiehost is not cleared when switching away from a custom Host:, the stale value is reused for outgoing cookie selection.

C3flaw supported

The stale state is also used for incoming Set-Cookie attribution

Read the reasoning

http_header_s() at line 3561-3562 uses data->state.aptr.cookiehost for Set-Cookie attribution if set, causing incoming cookies to be stored under the stale custom host instead of the actual request host.

C4flaw supported

The stale state is set from the custom-Host: path and is not cleared in the non-custom-host path

Read the reasoning

The custom-Host: path (lines 2030-2065) sets aptr->cookiehost. The non-custom-host path (lines 2075-2089) only sets aptr->host (the outgoing Host header) but never clears or resets aptr->cookiehost, leaving the stale value.

C5impact supported

A cookie seeded for victim.internal is leaked to attacker.test, and poisoned cookies are replayed back to victim.internal

Read the reasoning

The code path allows exactly the described attack: request 1 with custom Host: victim.internal sets cookiehost=victim.internal; request 2 to attacker.test (no custom Host:) sends victim.internal's cookies to attacker.test (line 2560) and attributes attacker.test's Set-Cookie to victim.internal (line 3561); request 3 to victim.internal replays the poisoned cookies.

C6reproduction supported

The PoC uses one easy handle with an in-memory cookie jar, sends request 1 with Host: victim.internal, removes that custom header for request 2 to attacker.test, and then sends request 3 back to victim.internal

Read the reasoning

The reproduction steps match the code flow: first request with custom Host: triggers the if-branch at line 2032 setting cookiehost; second request without custom Host: takes the else-branch at line 2075 which doesn't clear cookiehost; third request again takes the else-branch but cookiehost remains stale from request 1.

What the report says about other projects

confirmed The cookiehost value must be cleared when switching away from a custom Host: header to avoid cross-origin cookie leakage.

“When an origin server receives a request with an absolute-form of request-target, the origin server MUST ignore the received Host header field (if any) and instead use the host information of the request-target.” source

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary: libcurl keeps a stale `data->state.aptr.cookiehost` after a request that uses a custom `Host:` header. On later requests on the same easy handle, when no custom `Host:` is used, libcurl still reuses that stale value for outgoing cookie selection (`lib/http.c:2560-2563`) and incoming `Set-Cookie` attribution (`lib/http.c:3561-3567`). The stale state is set from the custom-`Host:` path in `lib/http.c:2030-2065` and is not cleared in the non-custom-host path at `lib/http.c:2075-2089`. I reproduced this on `curl 8.19.0 / libcurl 8.19.0` on x86_64 Linux. In the attached PoC, a cookie seeded for `victim.internal` is leaked to `attacker.test`, the attacker injects `poison=EVIL` into the victim jar, and that poisoned cookie is then replayed back to `victim.internal` on a third request. A ZIP with the PoC source, server script, captured logs, and a fuller write-up is attached. ## Affected version Reproduced on: `curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.5.5 zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.5 libssh2/1.11.1 nghttp2/1.68.1 ngtcp2/1.21.0 nghttp3/1.15.0 librtmp/2.3 mit-krb5/1.22.1 OpenLDAP/2.6.10` Platform: `Linux x86_64` ## Steps To Reproduce: 1. Start the attached local server: `python3 -u stale_cookiehost_server.py 18083` 2. Build the attached PoC: `cc poc_stale_cookiehost.c -o poc_stale_cookiehost $(curl-config --cflags --libs)` 3. Run the PoC: `./poc_stale_cookiehost` 4. The PoC uses one easy handle with an in-memory cookie jar, sends request 1 with `Host: victim.internal`, removes that custom header for request 2 to `attacker.test`, and then sends request 3 back to `victim.internal`. 5. Observe the server log: `{"host": "victim.internal", "path": "/seed", "cookie": null}` `{"host": "attacker.test:18083", "path": "/attack", "cookie": "sid=SECRET123"}` `{"host": "victim.internal:18083", "path": "/check", "cookie": "poison=EVIL; sid=SECRET123"}` 6. Observe the cookie jar output: after request 1: `sid=SECRET123` is stored for `victim.internal` after request 2: `poison=EVIL` is also stored for `victim.internal` after request 3: the poisoned cookie is replayed back to `victim.internal` ## Impact ## Summary: This breaks cookie origin isolation across reused easy handles. An attacker-controlled origin can receive cookies that belong to a different origin, inject its own cookies into tha …

Source code

lib/http.clines 2535–2588 · lines 2560-2563 cited by the report
2535 if(announced_exp100)
2536 result = http_exp100_add_reader(data);
2537
2538out:
2539 return result;
2540}
2541
2542#ifndef CURL_DISABLE_COOKIES
2543
2544static CURLcode http_cookies(struct Curl_easy *data,
2545 struct dynbuf *r)
2546{
2547 CURLcode result = CURLE_OK;
2548 char *addcookies = NULL;
2549 bool linecap = FALSE;
2550 if(data->set.str[STRING_COOKIE] &&
2551 !Curl_checkheaders(data, STRCONST("Cookie")))
2552 addcookies = data->set.str[STRING_COOKIE];
2553
2554 if(data->cookies || addcookies) {
2555 struct Curl_llist list;
2556 int count = 0;
2557
2558 if(data->cookies && data->state.cookie_engine) {
2559 bool okay;
2560 const char *host = data->state.aptr.cookiehost ?
C2: const char *host = data->state.aptr.cookiehost ? data->state.aptr.cookiehost : data->conn->host.name;
C5: Outgoing cookie selection uses stale cookiehost
2561 data->state.aptr.cookiehost : data->conn->host.name;
2562 Curl_share_lock(data, CURL_LOCK_DATA_COOKIE, CURL_LOCK_ACCESS_SINGLE);
2563 result = Curl_cookie_getlist(data, data->conn, &okay, host, &list);
2564 if(!result && okay) {
2565 struct Curl_llist_node *n;
2566 size_t clen = 8; /* hold the size of the generated Cookie: header */
2567
2568 /* loop through all cookies that matched */
2569 for(n = Curl_llist_head(&list); n; n = Curl_node_next(n)) {
2570 struct Cookie *co = Curl_node_elem(n);
2571 if(co->value) {
2572 size_t add;
2573 if(!count) {
2574 result = curlx_dyn_addn(r, STRCONST("Cookie: "));
2575 if(result)
2576 break;
2577 }
2578 add = strlen(co->name) + strlen(co->value) + 1;
2579 if(clen + add >= MAX_COOKIE_HEADER_LEN) {
2580 infof(data, "Restricted outgoing cookies due to header size, "
2581 "'%s' not sent", co->name);
2582 linecap = TRUE;
2583 break;
2584 }
2585 result = curlx_dyn_addf(r, "%s%s=%s", count ? "; " : "",
2586 co->name, co->value);
2587 if(result)
2588 break;
lib/http.clines 3536–3592 · lines 3561-3567 cited by the report
3536 }
3537 return CURLE_OK;
3538}
3539
3540/*
3541 * http_header_s() parses a single response header starting with S.
3542 */
3543static CURLcode http_header_s(struct Curl_easy *data,
3544 const char *hd, size_t hdlen)
3545{
3546#if !defined(CURL_DISABLE_COOKIES) || !defined(CURL_DISABLE_HSTS)
3547 struct connectdata *conn = data->conn;
3548 const char *v;
3549#else
3550 (void)data;
3551 (void)hd;
3552 (void)hdlen;
3553#endif
3554
3555#ifndef CURL_DISABLE_COOKIES
3556 v = (data->cookies && data->state.cookie_engine) ?
3557 HD_VAL(hd, hdlen, "Set-Cookie:") : NULL;
3558 if(v) {
3559 /* If there is a custom-set Host: name, use it here, or else use
3560 * real peer hostname. */
3561 const char *host = data->state.aptr.cookiehost ?
C3: const char *host = data->state.aptr.cookiehost ? data->state.aptr.cookiehost : conn->host.name;
C5: Incoming Set-Cookie attribution uses stale cookiehost
3562 data->state.aptr.cookiehost : conn->host.name;
3563 const bool secure_context = Curl_secure_context(conn, host);
3564 CURLcode result;
3565 Curl_share_lock(data, CURL_LOCK_DATA_COOKIE, CURL_LOCK_ACCESS_SINGLE);
3566 result = Curl_cookie_add(data, data->cookies, TRUE, FALSE, v, host,
3567 data->state.up.path, secure_context);
3568 Curl_share_unlock(data, CURL_LOCK_DATA_COOKIE);
3569 return result;
3570 }
3571#endif
3572#ifndef CURL_DISABLE_HSTS
3573 /* If enabled, the header is incoming and this is over HTTPS */
3574 v = (data->hsts &&
3575 (Curl_xfer_is_secure(data) ||
3576#ifdef DEBUGBUILD
3577 /* allow debug builds to circumvent the HTTPS restriction */
3578 getenv("CURL_HSTS_HTTP")
3579#else
3580 0
3581#endif
3582 )
3583 ) ? HD_VAL(hd, hdlen, "Strict-Transport-Security:") : NULL;
3584 if(v) {
3585 CURLcode check =
3586 Curl_hsts_parse(data->hsts, conn->host.name, v);
3587 if(check) {
3588 if(check == CURLE_OUT_OF_MEMORY)
3589 return check;
3590 infof(data, "Illegal STS header skipped");
3591 }
3592#ifdef DEBUGBUILD
lib/http.clines 2005–2114 · lines 2030-2065 cited by the report; lines 2075-2089 cited by the report
2005 curlx_free(data->state.aptr.uagent);
2006 data->state.aptr.uagent = NULL;
2007 }
2008 return CURLE_OK;
2009}
2010
2011static CURLcode http_set_aptr_host(struct Curl_easy *data)
2012{
2013 struct connectdata *conn = data->conn;
2014 struct dynamically_allocated_data *aptr = &data->state.aptr;
2015 const char *ptr;
2016
2017 if(!data->state.this_is_a_follow) {
2018 /* Free to avoid leaking memory on multiple requests */
2019 curlx_free(data->state.first_host);
2020
2021 data->state.first_host = curlx_strdup(conn->host.name);
2022 if(!data->state.first_host)
2023 return CURLE_OUT_OF_MEMORY;
2024
2025 data->state.first_remote_port = conn->remote_port;
2026 data->state.first_remote_protocol = conn->scheme->protocol;
2027 }
2028 curlx_safefree(aptr->host);
2029
2030 ptr = Curl_checkheaders(data, STRCONST("Host"));
C6: Custom Host: detection and cookiehost extraction
2031 if(ptr && (!data->state.this_is_a_follow ||
2032 curl_strequal(data->state.first_host, conn->host.name))) {
2033#ifndef CURL_DISABLE_COOKIES
2034 /* If we have a given custom Host: header, we extract the hostname in
2035 order to possibly use it for cookie reasons later on. We only allow the
2036 custom Host: header if this is NOT a redirect, as setting Host: in the
2037 redirected request is being out on thin ice. Except if the hostname
2038 is the same as the first one! */
2039 char *cookiehost;
2040 CURLcode result = copy_custom_value(ptr, &cookiehost);
2041 if(result)
2042 return result;
2043 if(!*cookiehost)
2044 /* ignore empty data */
2045 curlx_free(cookiehost);
2046 else {
2047 /* If the host begins with '[', we start searching for the port after
2048 the bracket has been closed */
2049 if(*cookiehost == '[') {
2050 char *closingbracket;
2051 /* since the 'cookiehost' is an allocated memory area that will be
2052 freed later we cannot increment the pointer */
2053 memmove(cookiehost, cookiehost + 1, strlen(cookiehost) - 1);
2054 closingbracket = strchr(cookiehost, ']');
2055 if(closingbracket)
2056 *closingbracket = 0;
2057 }
2058 else {
2059 int startsearch = 0;
2060 char *colon = strchr(cookiehost + startsearch, ':');
2061 if(colon)
2062 *colon = 0; /* The host must not include an embedded port number */
2063 }
2064 curlx_free(aptr->cookiehost);
2065 aptr->cookiehost = cookiehost;
C1: aptr->cookiehost = cookiehost; sets the cookiehost from custom Host: header
2066 }
2067#endif
2068
2069 if(!curl_strequal("Host:", ptr)) {
2070 aptr->host = curl_maprintf("Host:%s\r\n", &ptr[5]);
2071 if(!aptr->host)
2072 return CURLE_OUT_OF_MEMORY;
2073 }
2074 }
2075 else {
C4: else branch (non-custom-host path) sets aptr->host but does not clear aptr->cookiehost
C6: Non-custom-host path without cookiehost clearing
2076 /* Use the hostname as present in the URL if it was IPv6. */
2077 char *host = (data->state.up.hostname[0] == '[') ?
2078 data->state.up.hostname : conn->host.name;
2079
2080 if(((conn->given->protocol & (CURLPROTO_HTTPS | CURLPROTO_WSS)) &&
2081 (conn->remote_port == PORT_HTTPS)) ||
2082 ((conn->given->protocol & (CURLPROTO_HTTP | CURLPROTO_WS)) &&
2083 (conn->remote_port == PORT_HTTP)))
2084 /* if(HTTPS on port 443) OR (HTTP on port 80) then do not include
2085 the port number in the host string */
2086 aptr->host = curl_maprintf("Host: %s\r\n", host);
2087 else
2088 aptr->host = curl_maprintf("Host: %s:%d\r\n", host, conn->remote_port);
2089
C4: End of else branch - no clearing of cookiehost
2090 if(!aptr->host)
2091 /* without Host: we cannot make a nice request */
2092 return CURLE_OUT_OF_MEMORY;
2093 }
2094 return CURLE_OK;
2095}
2096
2097/*
2098 * Append the request-target to the HTTP request
2099 */
2100static CURLcode http_target(struct Curl_easy *data,
2101 struct dynbuf *r)
2102{
2103 CURLcode result = CURLE_OK;
2104 const char *path = data->state.up.path;
2105 const char *query = data->state.up.query;
2106#ifndef CURL_DISABLE_PROXY
2107 struct connectdata *conn = data->conn;
2108#endif
2109
2110 if(data->set.str[STRING_TARGET]) {
2111 path = data->set.str[STRING_TARGET];
2112 query = NULL;
2113 }
2114