HexBrain

curl · Report #4123190

CVE-2026-111954: upload reader failed redirect UAF

Validstatus
lowseverity
1upvotes
26 Oct 2026disclosed

How it was found

Summary:

Curl_req_hard_reset() (lib/request.c line 124 in curl-8_23_0) calls Curl_client_reset() while req->rewind_read may still be set, which keeps the client reader stack for a rewind (lib/sendf.c line 82), and only clears the flag at line 157, so the kept stack is never rewound or freed and is still installed when the next transfer on the same easy handle starts, since the hard reset runs at the start of every Curl_connect(). The flag is left set by http_perhapsrewind() when a server answers a request with a body with a redirect that then fails before Curl_http_follow() reaches its soft reset, for example a 307 with "Location: http://[::1" (CURLE_URL_MALFORMAT) or a 307 with a header line that has no colon (CURLE_WEIRD_SERVER_REPLY), both with CURLOPT_FOLLOWLOCATION. The next upload on that handle over ftp, ftps, sftp, scp, file or tftp does not install a reader of its own and reads through Curl_client_read() with the old stack (file.c line 369, ftp.c line 561), so when the application has replaced the postfields in between, which frees libcurl's own COPYPOSTFIELDS copy (setopt.c line 2026), cr_buf_read() memcpy()s 65536 bytes per read from freed memory (sendf.c line 1334) and sends them to the upload destination. Reproduced on curl-8_23_0 built with ASAN: heap-use-after-free READ of size 65536 in cr_buf_read from file_do and from Curl_req_send_more (ftp), freed by curl_easy_setopt(CURLOPT_POSTFIELDS) and allocated by curl_easy_setopt(CURLOPT_COPYPOSTFIELDS); the plain debug build crashes with SIGSEGV in the same memcpy with a 1 MB body since glibc unmaps that chunk, and with a 100 KB body the read is silent and transfer 2 reports success. The ordering came with a93113b5b4 (8.16.0), master is unchanged, and calling curl_easy_reset() between the transfers avoids it. A six line fix in Curl_pretransfer() that drops a pending rewind at the start of a new transfer makes the reproduction clean and leaves the test suite unchanged (patch attached); AI tooling helped me with the test programs, the reproduction and the numbers are mine.

Affected version

curl 8.23.0 built from the curl-8_23_0 tag (3163ec0656), Linux x86_64, OpenSSL 3.6.4, clang with -fsanitize=address, and a plain gcc --enable-debug build. By the commit it is 8.16.0 through 8.23.0. curl -V: curl 8.23.0-DEV (x86_64-pc-linux-gnu) libcurl/8.23.0-DEV OpenSSL/3.6.4 zlib/1.3.2 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.23.3 nghttp2/1.70.0 OpenLDAP/2.6.14

Steps To Reproduce:

  1. python3 http_307bad.py 8900 (attached: reads the request head and one body byte, answers "307" + "Location: http://[::1" + "Content-Length: 0", closes). For the second trigger run it with --badheader --location /ok.
  2. Build reader_reuse.c (attached) against the ASAN libcurl. It uses one easy handle: a POST with CURLOPT_COPYPOSTFIELDS (BODY bytes) and CURLOPT_FOLLOWLOCATION, then without curl_easy_reset() it sets CURLOPT_POSTFIELDS to NULL, CURLOPT_UPLOAD, CURLOPT_READDATA to a FILE* and a second URL.
  3. BODY=1000000 ./reader_reuse http://127.0.0.1:8900/post file:///tmp/out -> transfer 1 returns 3, then ASAN reports heap-use-after-free READ of size 65536 in cr_buf_read (sendf.c:1334) called from file_do (file.c:369), freed by setopt.c:2026, allocated by setopt.c:1811.
  4. Same with ftp://127.0.0.1:PORT/x as the second URL against any FTP server that accepts STOR: the same report, now from Curl_req_send_more (request.c:468); on the non ASAN debug build the process dies with SIGSEGV in memcpy from cr_buf_read.
  5. BODY=100000 (under the mmap threshold): no crash, transfer 2 returns 0 and the destination gets bytes from the freed buffer instead of the READDATA content.
  6. Control: add --reset (curl_easy_reset between the transfers): transfer 2 uses the READDATA file as expected.
  7. Apply fix-pretransfer-drop-stale-readers.patch, rebuild, repeat 3 and 4: clean, the real READDATA content is uploaded.

Impact

Summary:

A server that answers a redirect in a way curl cannot follow leaves libcurl with a reader stack that belongs to the previous request body, and the next upload on the same handle over one of the protocols that use the generic reader reads 64 KB blocks from memory libcurl already freed and sends them to wherever that upload goes, a remote FTP, SFTP or TFTP server included, or into a local file. The application only has to reuse its easy handle for more than one transfer, which is the documented and recommended way to use libcurl, replace the postfields, and not call curl_easy_reset() in between; the curl tool creates a new handle per transfer and is not affected, language bindings that keep one handle per object are. With a larger body the same read crashes the application.

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