HexBrain

curl · Report #3355213

Use-after-free when POST body buffer is freed before transfer

Informativestatus
mediumseverity
16upvotes
26 Sept 2025disclosed

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.

  1. 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
    
  2. Start a local HTTP server to ensure the client sends a request body:

    python3 -m http.server 18080 --bind 127.0.0.1 &
    
  3. Create the following PoC program (mirrors the misuse: set POSTFIELDSIZE, set POSTFIELDS to 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;
    }
    
  4. 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.c updates s->postfields and manages STRING_COPYPOSTFIELDS ownership on CURLOPT_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_COPYPOSTFIELDS with an explicit size and conversion, the wrapper passes the converted buffer to CURLOPT_POSTFIELDS and also assigns it to STRING_COPYPOSTFIELDS, but later frees the same buffer before returning.

    • Passing the converted buffer and assigning to STRING_COPYPOSTFIELDS:
    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.

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