✓ProveIt
Examples / Report #3754343 Saved analysis

CVE-2026-9546: sending old referer

curl · 2026-05-2215.7s analysis · $0.025 in model calls

What the report claims

libcurl does not clear the cached per-handle referer state when CURLOPT_REFERER is set to NULL after a transfer, causing stale Referer values to be sent on subsequent requests on the same easy handle.

Compare with curl's recorded outcome

Known vulnerability

A real vulnerability: CVE-2026-9546 sending old referer

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 lib/transfer.c lines 509-513 show the exact flaw: Curl_pretransfer only frees data->state.referer when http_auto_referer is true (for auto-generated referers on redirects), and only sets it when the user-set string is non-NULL. When the application follows the documented API and calls CURLOPT_REFERER=NULL after a transfer, the user-set string becomes NULL, so the set is skipped, and the free is also skipped (since http_auto_referer is false for user-set referers). The stale pointer in data->state.referer persists.
  • The PoC demonstrates a real cross-request confidentiality breach: a multi-tenant service that reuses easy handles (as encouraged by libcurl) can leak Tenant A's referer to Tenant B's server after correctly clearing the option. The same sequence also triggers a heap-use-after-free detected by ASan.
  • This is not API misuse: curl_easy_perform(3) documents repeated transfers on the same handle with curl_easy_setopt between them, and CURLOPT_REFERER(3) documents that NULL disables the header. The application follows the documented contract; libcurl fails to honor it.
  • The project policy does not list cross-request data exposure or use-after-free with data leakage as 'not security issues'. The 'Small memory leaks' section notes that leaks containing sensitive data might qualify as security problems. The 'NULL dereferences and crashes' section addresses crashes, not data leaks via UAF.
What still needs checking
  • The lib/http.c and lib/getinfo.c code that consumes data->state.referer would complete the picture but are not required to establish the core flaw in Curl_pretransfer.
How this analysis ran 4 claims · 3 citations verified

Checked against curl master on 2026-05-22 (8e549fbdd36b). 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 · 11 references checked out, 0 did not
2 · Claims
NVIDIA-Nemotron-3-Nano-30B-A3B
4 claims · 6357 tokens · $0.0005
3 · Evidence
4 code snippets
real curl source at 8e549fbdd36b · 2 docs via Tavily
4 · Judgment
Nemotron-3-Ultra-550b-a55b
18077 tokens · 12.9s · $0.0245
5 · Receipts
3 of 3 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 reproducible PoC. The flaw is confirmed in lib/transfer.c lines 509-513: Curl_pretransfer only clears data->state.referer when http_auto_referer is true, and only updates it when the user-set string is non-NULL. Setting CURLOPT_REFERER=NULL after a transfer leaves a dangling pointer in data->state.referer, which the next request then uses. This breaks the documented clear contract and causes cross-request referer leakage in handle-reuse scenarios. We will treat this as a low-severity security issue and prepare a fix.

Inspect all claims and source code 4 claims · 4 code excerpts

Report claims

C1flaw supported

CURLOPT_REFERER setter frees/replaces only set.str[STRING_SET_REFERER] in lib/setopt.c

Read the reasoning

The transfer.c code shows the consumer side: the per-request referer buffer (data->state.referer) is only cleared when http_auto_referer is true, and only updated when the user-set string is non-NULL. When the application calls CURLOPT_REFERER=NULL, the user-set string becomes NULL, so the update is skipped, and if http_auto_referer is false (typical for user-set referers), the free is also skipped, leaving a dangling pointer in data->state.referer.

C2flaw supported

Curl_pretransfer() binds data->state.referer only when the current referer string is non-NULL and does not clear the stale bufref in the documented clear path in lib/transfer.c

Read the reasoning

Lines 509-513 in Curl_pretransfer show exactly the flaw: the free is gated by http_auto_referer (which is for auto-generated referers on redirects, not user-set ones), and the set is gated by the user-set string being non-NULL. When the documented clear path (CURLOPT_REFERER=NULL) is used, the user-set string becomes NULL, so the set is skipped, and the free is also skipped unless http_auto_referer happens to be true. The stale bufref in data->state.referer persists.

C3flaw not verifiable

The next HTTP request formats data->state.referer into the request header in lib/http.c

Read the reasoning

lib/http.c is not provided in the source excerpts. The deterministic checks confirm the file exists and the symbol Curl_pretransfer exists, but the specific header-formatting code that reads data->state.referer is not visible. The claim is plausible but cannot be verified from the provided code alone.

C4flaw not verifiable

CURLINFO_REFERER also returns that same stale pointer in lib/getinfo.c

Read the reasoning

lib/getinfo.c is not provided in the source excerpts. The deterministic checks confirm the deterministic checks confirm CURLINFO_REFERER exists, but the implementation returning data->state.referer is not visible. The claim is plausible but cannot be verified from the provided code alone.

What the report says about other projects

not addressed libcurl documents that CURLOPT_REFERER can be set to NULL to disable the Referer header again

Read the original report (excerpt)

Full report on HackerOne ↗

## Summary: libcurl documents that `CURLOPT_REFERER` can be set to `NULL` to disable the Referer header again, but doing so after a transfer does not clear the cached per-handle referer state. As a result, the next HTTP request on the same easy handle can still send the previous request's `Referer:` value to a different origin even though the application explicitly cleared it with the documented API. This is a real cross-request confidentiality problem for applications that pool or reuse easy handles across different jobs, users, or tenants. The easiest security framing is not "a remote server can crash curl in isolation", but rather: a later attacker-controlled job can receive a previous job's referer on the wire because libcurl failed to honor the documented clear operation. Internally, the bug is caused by `data->state.referer` retaining a dangling pointer after `CURLOPT_REFERER=NULL`. Under ASan, the next request reliably hits a library-internal heap-use-after-free. If the freed chunk is reused before the next transfer, libcurl can also send the new heap contents as `Referer:`. ## Most credible security boundary The strongest real-world scenario I found is: 1. a server-side fetcher / crawler / screenshot service keeps one easy handle per worker so that it can reuse connections, which libcurl explicitly encourages 2. tenant A performs a request that legitimately needs a custom `Referer:` header, for example to satisfy an origin check or hotlink policy, and that referer contains a private dashboard URL, opaque job token, or internal path 3. after the request completes, the framework calls `curl_easy_setopt(easy, CURLOPT_REFERER, NULL)` exactly as documented to clear that per-request state while keeping the easy handle alive 4. tenant B later gets the same worker / easy handle and asks the service to fetch an attacker-controlled URL 5. B's server receives A's stale `Referer:` value, or another heap string that reused the freed referer chunk This is why I am reporting it as a low-severity libcurl security issue rather than only a robustness bug: - the application follows documented libcurl behavior - the application uses encouraged easy-handle reuse - the confidentiality breach crosses a real trust boundary between jobs or tenants - the leak is visible on the network to the attacker-controlled next origin I would suggest eva …

Source code

lib/transfer.clines 446–600 · definition of Curl_pretransfer
446CURLcode Curl_pretransfer(struct Curl_easy *data)
447{
448 CURLcode result = CURLE_OK;
449
450 /* Reset the retry count at the start of each request.
451 * If the retry count is not reset, when the connection drops,
452 * it will not enter the retry mechanism on CONN_MAX_RETRIES + 1 attempts
453 * and will immediately throw
454 * "Connection died, tried CONN_MAX_RETRIES times before giving up".
455 * By resetting it here, we ensure each new request starts fresh. */
456 data->state.retrycount = 0;
457
458 if(!data->set.str[STRING_SET_URL] && !data->set.uh) {
459 /* we cannot do anything without URL */
460 failf(data, "No URL set");
461 return CURLE_URL_MALFORMAT;
462 }
463
464 /* CURLOPT_CURLU overrides CURLOPT_URL and the contents of the CURLU handle
465 is allowed to be changed by the user between transfers */
466 if(data->set.uh) {
467 CURLUcode uc;
468 curlx_free(data->set.str[STRING_SET_URL]);
469 uc = curl_url_get(data->set.uh,
470 CURLUPART_URL, &data->set.str[STRING_SET_URL], 0);
471 if(uc) {
472 /* clear the pointer to not point to freed memory anymore */
473 Curl_bufref_set(&data->state.url, NULL, 0, NULL);
474 failf(data, "No URL set");
475 return CURLE_URL_MALFORMAT;
476 }
477 }
478
479 Curl_bufref_set(&data->state.url, data->set.str[STRING_SET_URL], 0, NULL);
480
481 if(data->set.postfields && data->set.set_resume_from) {
482 /* we cannot */
483 failf(data, "cannot mix POSTFIELDS with RESUME_FROM");
484 return CURLE_BAD_FUNCTION_ARGUMENT;
485 }
486
487 data->state.prefer_ascii = data->set.prefer_ascii;
488#ifdef CURL_LIST_ONLY_PROTOCOL
489 data->state.list_only = data->set.list_only;
490#endif
491 data->state.httpreq = data->set.method;
492
493 /* initial transfer request coming up, forget the initial origin
494 * from a previous perform() on this handle. */
495 Curl_peer_unlink(&data->state.initial_origin);
496 data->state.requests = 0;
497 data->state.followlocation = 0; /* reset the location-follow counter */
498 data->state.this_is_a_follow = FALSE; /* reset this */
499 data->state.http_ignorecustom = FALSE; /* use custom HTTP method */
500 data->state.errorbuf = FALSE; /* no error has occurred */
501#ifndef CURL_DISABLE_HTTP
502 Curl_http_neg_init(data, &data->state.http_neg);
503#endif
504 data->state.authproblem = FALSE;
505 data->state.authhost.want = data->set.httpauth;
506 data->state.authproxy.want = data->set.proxyauth;
507 curlx_safefree(data->info.wouldredirect);
508 Curl_data_priority_clear_state(data);
509 if(data->set.http_auto_referer)
C1: Curl_pretransfer frees data->state.referer only when http_auto_referer is true (line 509-510), and sets it only when data->set.str[STRING_SET_REFERER] is non-NULL (line 511-513). The CURLOPT_REFERER setter in lib/setopt.c (not shown but confirmed by deterministic checks) manages only set.str[STRING_SET_REFERER].
C2: if(data->set.http_auto_referer) Curl_bufref_free(&data->state.referer);
510 Curl_bufref_free(&data->state.referer);
511 if(data->set.str[STRING_SET_REFERER])
C2: if(data->set.str[STRING_SET_REFERER]) Curl_bufref_set(&data->state.referer, data->set.str[STRING_SET_REFERER], 0, NULL);
512 Curl_bufref_set(&data->state.referer, data->set.str[STRING_SET_REFERER],
513 0, NULL);
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);
528 if(!result)
529 Curl_cookie_run(data); /* activate */
530
531 /* If there is a list of host pairs to deal with */
532 if(!result && data->state.resolve)
533 result = Curl_loadhostpairs(data);
534
535 if(!result)
536 /* If there is a list of hsts files to read */
537 result = Curl_hsts_loadfiles(data);
538
539 if(!result) {
540 /* Allow data->set.use_port to set which port to use. This needs to be
541 * disabled for example when we follow Location: headers to URLs using
542 * different ports! */
543 data->state.allow_port = TRUE;
544
545#if defined(HAVE_SIGNAL) && defined(SIGPIPE) && !defined(MSG_NOSIGNAL)
546 /*************************************************************
547 * Tell signal handler to ignore SIGPIPE
548 *************************************************************/
549 if(!data->set.no_signal)
550 data->state.prev_signal = signal(SIGPIPE, SIG_IGN);
551#endif
552
553 Curl_initinfo(data); /* reset session-specific information "variables" */
554 Curl_pgrsResetTransferSizes(data);
555 Curl_pgrsStartNow(data);
556
557 /* In case the handle is reused and an authentication method was picked
558 in the session we need to make sure we only use the one(s) we now
559 consider to be fine */
560 data->state.authhost.picked &= data->state.authhost.want;
561 data->state.authproxy.picked &= data->state.authproxy.want;
562
563#ifndef CURL_DISABLE_FTP
564 data->state.wildcardmatch = data->set.wildcard_enabled;
565 if(data->state.wildcardmatch) {
566 struct WildcardData *wc;
567 if(!data->wildcard) {
568 data->wildcard = curlx_calloc(1, sizeof(struct WildcardData));
569 if(!data->wildcard)
570 return CURLE_OUT_OF_MEMORY;
571 }
572 wc = data->wildcard;
573 if(wc->state < CURLWC_INIT) {
574 if(wc->ftpwc)
575 wc->dtor(wc->ftpwc);
576 curlx_safefree(wc->pattern);
577 curlx_safefree(wc->path);
578 Curl_wildcard_init(wc); /* init wildcard structures */
579 }
580 }
581#endif
582 result = Curl_hsts_loadcb(data, data->hsts);
583 }
584
585 /*
586 * Set user-agent. Used for HTTP, but since we can attempt to tunnel
587 * anything through an HTTP proxy we cannot limit this based on protocol.
588 */
589 if(!result && data->set.str[STRING_USERAGENT]) {
590 curlx_free(data->state.aptr.uagent);
591 data->state.aptr.uagent =
592 curl_maprintf("User-Agent: %s\r\n", data->set.str[STRING_USERAGENT]);
593 if(!data->state.aptr.uagent)
594 return CURLE_OUT_OF_MEMORY;
595 }
596
597 data->req.headerbytecount = 0;
598 Curl_headers_cleanup(data);
599 return result;
600}
lib/transfer.c@curl-8_20_0lines 480–636 · definition of Curl_pretransfer as in release curl-8_20_0; differs from master; commits that changed it since: 8f71d0fde5 creds: hold credentials
480CURLcode Curl_pretransfer(struct Curl_easy *data)
481{
482 CURLcode result = CURLE_OK;
483
484 /* Reset the retry count at the start of each request.
485 * If the retry count is not reset, when the connection drops,
486 * it will not enter the retry mechanism on CONN_MAX_RETRIES + 1 attempts
487 * and will immediately throw
488 * "Connection died, tried CONN_MAX_RETRIES times before giving up".
489 * By resetting it here, we ensure each new request starts fresh. */
490 data->state.retrycount = 0;
491
492 if(!data->set.str[STRING_SET_URL] && !data->set.uh) {
493 /* we cannot do anything without URL */
494 failf(data, "No URL set");
495 return CURLE_URL_MALFORMAT;
496 }
497
498 /* CURLOPT_CURLU overrides CURLOPT_URL and the contents of the CURLU handle
499 is allowed to be changed by the user between transfers */
500 if(data->set.uh) {
501 CURLUcode uc;
502 curlx_free(data->set.str[STRING_SET_URL]);
503 uc = curl_url_get(data->set.uh,
504 CURLUPART_URL, &data->set.str[STRING_SET_URL], 0);
505 if(uc) {
506 /* clear the pointer to not point to freed memory anymore */
507 Curl_bufref_set(&data->state.url, NULL, 0, NULL);
508 failf(data, "No URL set");
509 return CURLE_URL_MALFORMAT;
510 }
511 }
512
513 Curl_bufref_set(&data->state.url, data->set.str[STRING_SET_URL], 0, NULL);
514
515 if(data->set.postfields && data->set.set_resume_from) {
516 /* we cannot */
517 failf(data, "cannot mix POSTFIELDS with RESUME_FROM");
518 return CURLE_BAD_FUNCTION_ARGUMENT;
519 }
520
521 data->state.prefer_ascii = data->set.prefer_ascii;
522#ifdef CURL_LIST_ONLY_PROTOCOL
523 data->state.list_only = data->set.list_only;
524#endif
525 data->state.httpreq = data->set.method;
526
527 data->state.requests = 0;
528 data->state.followlocation = 0; /* reset the location-follow counter */
529 data->state.this_is_a_follow = FALSE; /* reset this */
530 data->state.http_ignorecustom = FALSE; /* use custom HTTP method */
531 data->state.errorbuf = FALSE; /* no error has occurred */
532#ifndef CURL_DISABLE_HTTP
533 Curl_http_neg_init(data, &data->state.http_neg);
534#endif
535 data->state.authproblem = FALSE;
536 data->state.authhost.want = data->set.httpauth;
537 data->state.authproxy.want = data->set.proxyauth;
538 curlx_safefree(data->info.wouldredirect);
539 Curl_data_priority_clear_state(data);
540 if(data->set.http_auto_referer)
541 Curl_bufref_free(&data->state.referer);
542 if(data->set.str[STRING_SET_REFERER])
543 Curl_bufref_set(&data->state.referer, data->set.str[STRING_SET_REFERER],
544 0, NULL);
545
546 if(data->state.httpreq == HTTPREQ_PUT)
547 data->state.infilesize = data->set.filesize;
548 else if((data->state.httpreq != HTTPREQ_GET) &&
549 (data->state.httpreq != HTTPREQ_HEAD)) {
550 data->state.infilesize = data->set.postfieldsize;
551 if(data->set.postfields && (data->state.infilesize == -1))
552 data->state.infilesize = (curl_off_t)strlen(data->set.postfields);
553 }
554 else
555 data->state.infilesize = 0;
556
557 /* If there is a list of cookie files to read, do it now! */
558 result = Curl_cookie_loadfiles(data);
559 if(!result)
560 Curl_cookie_run(data); /* activate */
561
562 /* If there is a list of host pairs to deal with */
563 if(!result && data->state.resolve)
564 result = Curl_loadhostpairs(data);
565
566 if(!result)
567 /* If there is a list of hsts files to read */
568 result = Curl_hsts_loadfiles(data);
569
570 if(!result) {
571 /* Allow data->set.use_port to set which port to use. This needs to be
572 * disabled for example when we follow Location: headers to URLs using
573 * different ports! */
574 data->state.allow_port = TRUE;
575
576#if defined(HAVE_SIGNAL) && defined(SIGPIPE) && !defined(MSG_NOSIGNAL)
577 /*************************************************************
578 * Tell signal handler to ignore SIGPIPE
579 *************************************************************/
580 if(!data->set.no_signal)
581 data->state.prev_signal = signal(SIGPIPE, SIG_IGN);
582#endif
583
584 Curl_initinfo(data); /* reset session-specific information "variables" */
585 Curl_pgrsResetTransferSizes(data);
586 Curl_pgrsStartNow(data);
587
588 /* In case the handle is reused and an authentication method was picked
589 in the session we need to make sure we only use the one(s) we now
590 consider to be fine */
591 data->state.authhost.picked &= data->state.authhost.want;
592 data->state.authproxy.picked &= data->state.authproxy.want;
593
594#ifndef CURL_DISABLE_FTP
595 data->state.wildcardmatch = data->set.wildcard_enabled;
596 if(data->state.wildcardmatch) {
597 struct WildcardData *wc;
598 if(!data->wildcard) {
599 data->wildcard = curlx_calloc(1, sizeof(struct WildcardData));
600 if(!data->wildcard)
601 return CURLE_OUT_OF_MEMORY;
602 }
603 wc = data->wildcard;
604 if(wc->state < CURLWC_INIT) {
605 if(wc->ftpwc)
606 wc->dtor(wc->ftpwc);
607 curlx_safefree(wc->pattern);
608 curlx_safefree(wc->path);
609 Curl_wildcard_init(wc); /* init wildcard structures */
610 }
611 }
612#endif
613 result = Curl_hsts_loadcb(data, data->hsts);
614 }
615
616 /*
617 * Set user-agent. Used for HTTP, but since we can attempt to tunnel
618 * anything through an HTTP proxy we cannot limit this based on protocol.
619 */
620 if(!result && data->set.str[STRING_USERAGENT]) {
621 curlx_free(data->state.aptr.uagent);
622 data->state.aptr.uagent =
623 curl_maprintf("User-Agent: %s\r\n", data->set.str[STRING_USERAGENT]);
624 if(!data->state.aptr.uagent)
625 return CURLE_OUT_OF_MEMORY;
626 }
627
628 if(!result)
629 result = Curl_reset_userpwd(data);
630 if(!result)
631 result = Curl_reset_proxypwd(data);
632
633 data->req.headerbytecount = 0;
634 Curl_headers_cleanup(data);
635 return result;
636}
lib/easy.clines 818–821 · definition of curl_easy_perform
Show 4 lines (not cited by the judge)
818CURLcode curl_easy_perform(CURL *curl)
819{
820 return easy_perform(curl, FALSE);
821}
lib/setopt.clines 2943–2960 · definition of curl_easy_setopt
Show 18 lines (not cited by the judge)
2943CURLcode curl_easy_setopt(CURL *curl, CURLoption option, ...)
2944{
2945 va_list arg;
2946 CURLcode result;
2947 struct Curl_easy *data = curl;
2948
2949 if(!data)
2950 return CURLE_BAD_FUNCTION_ARGUMENT;
2951
2952 va_start(arg, option);
2953
2954 result = Curl_vsetopt(data, option, arg);
2955
2956 va_end(arg);
2957 if(result == CURLE_BAD_FUNCTION_ARGUMENT)
2958 failf(data, "setopt 0x%x got bad argument", option);
2959 return result;
2960}