HexBrain

curl · Report #3774279

heap-use-after-free in state.referer when CURLOPT_REFERER replaced or cleared after perform

Informativestatus
lowseverity
22upvotes
11 Jun 2026disclosed

How it was found

Calling curl_easy_setopt(curl, CURLOPT_REFERER, ...) to replace or clear a previously-set referer after curl_easy_perform() frees the old string via Curl_setstropt (lib/setopt.c:87) but leaves data->state.referer.ptr pointing at the freed heap region. curl_easy_getinfo(CURLINFO_REFERER) and curl_easy_duphandle() then read through the dangling pointer. ASAN catches all three vectors reliably. Read-only, in-process UAF - the leaked bytes do NOT reach the wire.

This is an incomplete-fix variant of #21741. The 2026-05-25 fix 862e8a74a covers the next-perform window (clears state.referer in Curl_pretransfer when set to NULL), but does not cover the getinfo and duphandle paths between setopt and the next perform, and does not handle replacement (non-NULL to non-NULL) at all.

The regression was introduced by fa057ea3d (2026-04-15) which switched state.referer to a bufref aliasing set.str[STRING_SET_REFERER] without a setopt mirror. No released version is affected - 8.20.0 predates fa057ea3d.

Affected version

Tested on master HEAD 872c313d7 (2026-06-01). The relevant sites (lib/transfer.c:511-513, lib/setopt.c:2027, lib/easy.c:1022-1027, lib/getinfo.c:145) are unchanged since fa057ea3d.

curl 8.14.0-DEV (x86_64-pc-linux-gnu) libcurl/8.14.0-DEV OpenSSL/3.5.0 zlib/1.3.1
Built with ASAN+UBSan: -fsanitize=address,undefined -O0 -g

Steps To Reproduce

Build curl master with ASAN:

git clone https://github.com/curl/curl.git && cd curl
cmake -S . -B build \
  -DCMAKE_BUILD_TYPE=Debug \
  -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O0" \
  -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" \
  -DCMAKE_SHARED_LINKER_FLAGS="-fsanitize=address,undefined" \
  -DCURL_USE_OPENSSL=ON -DBUILD_SHARED_LIBS=ON \
  -DBUILD_TESTING=OFF -DBUILD_CURL_EXE=OFF
cmake --build build -j --target libcurl_shared

Vector 1 - getinfo after replacement (uaf_referer.c):

#include <curl/curl.h>
#include <stdio.h>
#include <string.h>
static size_t devnull(void *p, size_t s, size_t n, void *u){(void)p;(void)u;return s*n;}
int main(void){
    CURL *curl; const char *ref = NULL;
    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    curl_easy_setopt(curl, CURLOPT_URL, "file:///dev/null");
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, devnull);
    curl_easy_setopt(curl, CURLOPT_REFERER, "http://example.com/secret-A");
    curl_easy_perform(curl);
    /* frees old copy; state.referer dangles */
    curl_easy_setopt(curl, CURLOPT_REFERER, "http://example.com/B");
    curl_easy_getinfo(curl, CURLINFO_REFERER, &ref);
    if(ref) printf("strlen=%zu\n", strlen(ref)); /* UAF read */
    curl_easy_cleanup(curl);
    curl_global_cleanup();
}
gcc -O0 -g -fsanitize=address,undefined uaf_referer.c \
    -Iinclude -Lbuild/lib -lcurl-d -Wl,-rpath,build/lib -o uaf_referer
ASAN_OPTIONS=detect_leaks=0 ./uaf_referer

ASAN output:

==PID==ERROR: AddressSanitizer: heap-use-after-free on address ...
READ of size 2 ... thread T0
    #0 strlen
    #1 main uaf_referer.c:...
freed by thread T0 here:
    #1 Curl_setstropt lib/setopt.c:87
    #2 setopt_cptr lib/setopt.c:2027
previously allocated by thread T0 here:
    #1 Curl_setstropt lib/setopt.c:93

Vector 2 - getinfo after setopt to NULL (uaf_referer_null.c): Same flow but the replacement is NULL. The 2026-05-25 fix clears state.referer in the next Curl_pretransfer, but getinfo between setopt and perform still UAFs. ASAN trace attached.

Vector 3 - duphandle after replacement (uaf_referer_dup.c):

curl_easy_setopt(curl, CURLOPT_REFERER, "http://example.com/secret-A");
curl_easy_perform(curl);
curl_easy_setopt(curl, CURLOPT_REFERER, "http://example.com/B");
CURL *dup = curl_easy_duphandle(curl); /* strdup on dangling ptr */

curl_easy_duphandle calls Curl_bufref_dup (lib/easy.c:1024) which runs strdup() on the dangling pointer - ASAN catches this as a heap-use-after-free read.

Note: the leaked bytes do NOT reach the wire. The dup handle's next curl_easy_perform() re-aliases state.referer from set.str[STRING_SET_REFERER] in Curl_pretransfer (lib/transfer.c:511-515) before the Referer: header builder (lib/http.c:2941) runs. Verified empirically with a localhost listener: the dup handle transmitted the current referer, not the freed bytes.

Root cause: CURLOPT_URL at lib/setopt.c:2202-2208 mirrors the setopt into state.url via Curl_bufref_set. CURLOPT_REFERER at lib/setopt.c:2027 does not have this mirror - the asymmetry is the bug.

Suggested fix: Mirror CURLOPT_REFERER setopt into state.referer (matching the CURLOPT_URL pattern), or change Curl_bufref_set in lib/transfer.c:512 to Curl_bufref_memdup0 so state.referer owns its own copy.

Again: there is no bug-bounty for curl. We do not offer any rewards, only proper credits.

Understood. Submitting for CVE assignment and credit.

Impact

  • Read-only, in-process heap-use-after-free of the freed referer string. The read may land on another live allocation (credentials, headers, cookies) depending on heap reuse.
  • Vectors 1+2: the dangling pointer is returned to the application via CURLINFO_REFERER. The docs guarantee this pointer is valid until curl_easy_cleanup - the UAF violates that documented contract. Any application that dereferences, logs, or copies the returned value reads freed heap.
  • Vector 3: curl_easy_duphandle() strdup's the dangling pointer (the UAF read itself). The leaked bytes land in the dup handle's state.referer but are overwritten by Curl_pretransfer before any network I/O - so this is an in-process UAF read only, same impact class as vectors 1+2.
  • No released version is affected - fa057ea3d (2026-04-15) introduced the regression post-8.20.0.

This report was disclosed publicly on HackerOne and belongs to its author and the program. See the original