curl · Report #3774279
heap-use-after-free in state.referer when CURLOPT_REFERER replaced or cleared after perform
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 untilcurl_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'sstate.refererbut are overwritten byCurl_pretransferbefore 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.