curl · Report #3355213
Use-after-free when POST body buffer is freed before transfer
How it was found
Summary:
I locally reproduced a heap use-after-free in libcurl by setting CURLOPT_POSTFIELDSIZE and CURLOPT_POSTFIELDS to a heap buffer and then freeing that buffer before curl_easy_perform. AddressSanitizer (ASan) reports a heap-use-after-free read during the request send path. This demonstrates the class of bug that occurs when the POST data buffer handed to libcurl is freed prior to the transfer.
AI usage: The PoC and validation were performed manually on my machine. AI assistance was used to help structure this write-up.
Affected version
Built from the repository I got from github today (CMake configure reported: curl version=[8.17.0-DEV]) on macOS with AppleClang and ASan.
From CMake configure (observed during my build):
- Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss
- Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz NTLM SSL threadsafe TLS-SRP UnixSockets zstd
- Enabled SSL backends: OpenSSL v3+
Steps To Reproduce:
These are the steps I ran locally to reproduce the heap use-after-free under ASan.
-
Configure and build libcurl with AddressSanitizer:
cmake -S . -B build-asan \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DBUILD_SHARED_LIBS=ON \ -DCURL_USE_LIBPSL=OFF \ -DCMAKE_C_FLAGS='-fsanitize=address -fno-omit-frame-pointer' \ -DCMAKE_SHARED_LINKER_FLAGS='-fsanitize=address' \ -DCMAKE_EXE_LINKER_FLAGS='-fsanitize=address' cmake --build build-asan -j 8 -
Start a local HTTP server to ensure the client sends a request body:
python3 -m http.server 18080 --bind 127.0.0.1 & -
Create the following PoC program (mirrors the misuse: set
POSTFIELDSIZE, setPOSTFIELDSto a malloc'd buffer, then free it before perform):#include <curl/curl.h> #include <stdlib.h> #include <string.h> #include <stdio.h> int main(void) { curl_global_init(CURL_GLOBAL_DEFAULT); CURL *h = curl_easy_init(); if(!h) return 1; const char *url = "http://127.0.0.1:18080/"; curl_easy_setopt(h, CURLOPT_URL, url); curl_easy_setopt(h, CURLOPT_VERBOSE, 1L); curl_easy_setopt(h, CURLOPT_CONNECTTIMEOUT_MS, 2000L); curl_easy_setopt(h, CURLOPT_TIMEOUT_MS, 3000L); const char payload[] = "test data"; size_t n = sizeof(payload) - 1; char *cp = (char*)malloc(n); memcpy(cp, payload, n); curl_easy_setopt(h, CURLOPT_POSTFIELDSIZE, (long)n); curl_easy_setopt(h, CURLOPT_POSTFIELDS, cp); free(cp); // use-after-free when libcurl sends the request body CURLcode res = curl_easy_perform(h); fprintf(stderr, "perform result: %d\n", (int)res); curl_easy_cleanup(h); curl_global_cleanup(); return 0; } -
Compile and run the PoC against the locally built ASan libcurl (run from the repo root):
# compile the PoC against the freshly built ASan libcurl cc -fsanitize=address -fno-omit-frame-pointer -I include \ build-asan/poc_uaf.c build-asan/lib/libcurl.dylib \ -o build-asan/poc_uaf_local # run with ASan and ensure the executable finds the local libcurl DYLD_LIBRARY_PATH=build-asan/lib \ ASAN_OPTIONS=halt_on_error=1:detect_invalid_pointer_pairs=1:strict_string_checks=1:strict_memcmp=1 \ ./build-asan/poc_uaf_local
Supporting Material/References:
Below is the actual ASan report I observed when running the PoC:
* Trying 127.0.0.1:18080...
* Established connection to 127.0.0.1 (127.0.0.1 port 18080) from 127.0.0.1 port XXXXX
* using HTTP/1.x
=================================================================
==XXXXX==ERROR: AddressSanitizer: heap-use-after-free on address 0xXXXXXXXXXXXX at pc 0xXXXXXXXXXXXX bp 0xXXXXXXXXXXXX sp 0xXXXXXXXXXXXX
READ of size 9 at 0xXXXXXXXXXXXX thread T0
#0 0xXXXXXXXX in __asan_memcpy+0xXXX (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
#1 0xXXXXXXXX in cr_buf_read sendf.c:1316
#2 0xXXXXXXXX in Curl_client_read sendf.c:1223
#3 0xXXXXXXXX in add_from_client request.c:349
#4 0xXXXXXXXX in Curl_bufq_sipn bufq.c:574
#5 0xXXXXXXXX in Curl_req_send request.c:407
#6 0xXXXXXXXX in Curl_http http.c:2926
#7 0xXXXXXXXX in multi_runsingle multi.c:2508
#8 0xXXXXXXXX in curl_multi_perform multi.c:2771
#9 0xXXXXXXXX in curl_easy_perform easy.c:846
#10 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)
0xXXXXXXXXXXXX is located 0 bytes inside of 9-byte region [...]
freed by thread T0 here:
#0 0xXXXXXXXX in wrap_free (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
#1 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)
previously allocated by thread T0 here:
#0 0xXXXXXXXX in wrap_malloc (libclang_rt.asan_osx_dynamic.dylib+0xXXXXX)
#1 0xXXXXXXXX in main (poc_uaf_local+0xXXXX)
SUMMARY: AddressSanitizer: heap-use-after-free in __asan_memcpy
==XXXXX==ABORTING
This error consistently reproduces with the steps above.
#### Code locations (observed)
- Read path where ASan flagged the UAF (based on backtrace): `lib/sendf.c` in `cr_buf_read` where the request body buffer is memcpy'ed during send, e.g. around this function:
```1300:1323:curl/lib/sendf.c
static CURLcode cr_buf_read(struct Curl_easy *data,
struct Curl_creader *reader,
char *buf, size_t blen,
size_t *pnread, bool *peos)
{
struct cr_buf_ctx *ctx = reader->ctx;
size_t nread = ctx->blen - ctx->index;
...
memcpy(buf, ctx->buf + ctx->index, nread);
...
return CURLE_OK;
}
- The exact memcpy line in the current tree:
if(nread > blen)
nread = blen;
memcpy(buf, ctx->buf + ctx->index, nread);
*pnread = nread;
- For context on how user-supplied POST data may be stored:
lib/setopt.cupdatess->postfieldsand managesSTRING_COPYPOSTFIELDSownership onCURLOPT_COPYPOSTFIELDS/CURLOPT_POSTFIELDS.
case CURLOPT_COPYPOSTFIELDS:
...
s->postfields = s->str[STRING_COPYPOSTFIELDS];
s->method = HTTPREQ_POST;
break;
case CURLOPT_POSTFIELDS:
s->postfields = ptr;
/* Release old copied data. */
Curl_safefree(s->str[STRING_COPYPOSTFIELDS]);
s->method = HTTPREQ_POST;
break;
-
OS/400 CCSID wrapper (root cause pattern): when handling
CURLOPT_COPYPOSTFIELDSwith an explicit size and conversion, the wrapper passes the converted buffer toCURLOPT_POSTFIELDSand also assigns it toSTRING_COPYPOSTFIELDS, but later frees the same buffer before returning.- Passing the converted buffer and assigning to
STRING_COPYPOSTFIELDS:
- Passing the converted buffer and assigning to
result = curl_easy_setopt(easy, CURLOPT_POSTFIELDS, s);
data->set.str[STRING_COPYPOSTFIELDS] = s; /* Give to library. */
break;
- Freeing the buffer at function end:
va_end(arg);
free(cp);
return result;
Impact
Summary:
Based on direct observation, freeing the application-supplied POST body buffer after setting it with CURLOPT_POSTFIELDS (with an explicit CURLOPT_POSTFIELDSIZE) causes libcurl to dereference freed memory during the transfer, leading to a process crash (heap-use-after-free read). While this demonstration is a local misuse PoC, it shows the concrete risk of memory corruption/DoS when a client inadvertently frees the POST buffer before curl_easy_perform.