A high-severity memory safety vulnerability in the ubiquitous libcurl networking library (tracked as CVE-2026-92392) allowed malicious HTTP/2 servers to trigger a heap use-after-free condition on client applications performing streaming uploads. Documented in coordinated public disclosure via HackerOne (Report #4030333) by security researcher onn4a, the defect stems from complex asynchronous lifecycle desynchronization between paused client upload buffers, multiplexed nghttp2 stream cancellation frames, and connection pool retry logic.
| Specification Parameter | Vulnerability Detail & Telemetry | Technical Risk & Exploit Impact |
|---|---|---|
| Target Component | lib/cf-h2.c (HTTP/2 connection filter) / lib/multi.c / lib/transfer.c |
Core multiplexing transport engine responsible for HTTP/2 streaming in libcurl. |
| Vulnerability Identifier | CVE-2026-92392 / CWE-416 (Use After Free) | Dangling pointer dereference during connection pool ingress processing. |
| Severity Classification | High (CVSS 8.1) — CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:H |
Remote client memory corruption / heap exploitation when uploading files to malicious servers. |
| Affected Releases | libcurl versions prior to 8.11.0 (verified on 8.22.0 development build) | Affects command-line curl and thousands of downstream applications linking against libcurl.so.4. |
| Reporter Attribution | Security Researcher onn4a |
Disclosed via Internet Bug Bounty on HackerOne with AddressSanitizer verification. |
The HTTP/2 Streaming Upload Architecture in libcurl
Modern implementations of HTTP/2 multiplex multiple independent request/response streams over a single underlying TCP/TLS connection. In libcurl, this multiplexing is orchestrated by the HTTP/2 connection filter (lib/cf-h2.c), working in tandem with the third-party framing library nghttp2. Each transfer is represented by a struct Curl_easy handle, while stream-specific state (such as stream identifiers, receive windows, and send buffers) is maintained within struct cf_h2_stream.
When an application uploads a file via curl -T <file>, the upload progresses through the multi-interface state machine. Data flows from the client read callback into the request buffer (req.sendbuf). From there, the connection filter feeds data into nghttp2_submit_request(), attaching the struct Curl_easy pointer (data) as stream user data so that subsequent framing callbacks know which transfer is generating or receiving traffic.
In high-throughput environments, backpressure is managed through HTTP/2 flow control windows. If the remote server stops granting stream or connection window updates (or grants a window of 0), libcurl's upload stalls. The read callback returns NGHTTP2_ERR_DEFERRED, parking leftover unsent request bytes inside req.sendbuf while waiting for the peer to dispatch WINDOW_UPDATE frames.
Root Cause: Asynchronous Lifecycle Desynchronization
The vulnerability manifests when a malicious or non-compliant HTTP/2 server exploits the stall condition to force an asynchronous connection retry while un-transmitted bytes remain buffered.
The researcher mapped the entire control flow across six distinct phases, illustrating how two conflicting pointers are stored in nghttp2:
PHASE 1: Upload Starts
Curl_req_send() request.c:380 request head + body -> req.sendbuf
└─ req_flush() -> cf_h2_send() http2.c:2197 no stream yet
└─ h2_submit() http2.c:2062
├─ http2_data_setup() http2.c:361 streams[mid] created
└─ nghttp2_submit_request(..., data) http2.c:2133 stream 1 -> data <-- Pointer #1 stored
PHASE 2: Upload Stalls (Server Grants 0 Flow-Control Window)
multistate_performing() -> Curl_sendrecv() multi.c:2029 -> transfer.c:354
└─ sendrecv_ul() -> Curl_req_send_more() transfer.c:340 -> request.c:458
├─ add_from_client() request.c:351 read file -> req.sendbuf (64 KB)
└─ req_flush() -> cf_h2_send() http2.c:2197
└─ cf_h2_body_send() -> stream->sendbuf
└─ req_body_read_callback() http2.c:1577 window is 0 -> NGHTTP2_ERR_DEFERRED
=> Leftover bytes remain parked in req.sendbuf
PHASE 3: Server Sends HEADERS(200) + RST_STREAM(HTTP_1_1_REQUIRED)
multistate_performing() multi.c:2029
├─ sendrecv_dl() -> xfer_recv_resp() transfer.c:269
│ └─ cf_h2_recv() -> h2_progress_ingress() http2.c:1972
│ └─ nghttp2_session_mem_recv() parses RST_STREAM
│ └─ on_stream_close() http2.c:1298
│ ├─ stream->closed = TRUE http2.c:1333
│ └─ set_stream_user_data(0) http2.c:1346 stream 1 pointer cleared
├─ if(result == CURLE_HTTP2_STREAM && Curl_h2_http_1_1_error())
│ retry = TRUE; follow = FOLLOW_RETRY multi.c:2087
│
│ PHASE 4: Wrap-Up & Accidental Re-Submission
├─ multi_done(data, CURLE_OK, premature=FALSE) multi.c:2130
│ ├─ cf_cntrl_all(CF_CTRL_DATA_DONE) cfilters.c:448
│ │ └─ http2_data_done() http2.c:416
│ │ └─ hash_remove(streams, mid) http2.c:447 <-- Filter forgets transfer
│ ├─ result = Curl_req_done(aborted=FALSE) multi.c:754 -> request.c:102
│ │ └─ req_flush() -> xfer_send() request.c:298 req.sendbuf not empty!
│ │ └─ cf_h2_send() http2.c:2197 stream == NULL -> interpreted as "new request"
│ │ └─ h2_submit() http2.c:2062
│ │ └─ nghttp2_submit_request(..., data) stream 3 -> data <-- Pointer #2 stored!
│ └─ multi_done_locked() multi.c:632
│ └─ Curl_cpool_conn_now_idle() conn back in pool, stream 3 still -> data
└─ result = multi_follow(FOLLOW_RETRY) multi.c:2132 retry on new HTTP/1.1 connection
PHASE 5: Retry Finishes, Easy Handle Freed
del_per_transfer() src/tool_operate.c:240
└─ curl_easy_cleanup() -> Curl_close() url.c:197
└─ free(data) url.c:303 <-- Pointer #2 now dangling!
PHASE 6: Connection Pool Teardown Triggers UAF
Curl_cpool_destroy() -> cf_h2_shutdown() http2.c:2559
└─ h2_progress_ingress() http2.c:1865
└─ nghttp2_session_mem_recv() server sends HEADERS for stream 3
└─ on_begin_headers() http2.c:1355
├─ data_s = get_stream_user_data(3) http2.c:1364 returns freed pointer!
└─ H2_STREAM_CTX(ctx, data_s) http2.c:1373 dereferences data_s->mid <-- CRASH / UAF
Exploitation Mechanics & AddressSanitizer Crash Walkthrough
To demonstrate the vulnerability end-to-end, researcher onn4a provided a malicious Python server (evil-server.py) that accepts TLS connections on port 19170. When an ASan-instrumented build of curl 8.22.0 uploads a document:
# Run curl with AddressSanitizer against the test server:
./src/curl --insecure -T ./sample_docs.md https://127.0.0.1:19170/x
The server intentionally delays the flow-control window until req.sendbuf is saturated with 64 KB of file data, then dispatches an RST_STREAM frame with error code 0xd (HTTP_1_1_REQUIRED). The connection filter cleanly clears pointer #1. But when Curl_req_done() executes during wrap-up, the un-cleared buffer forces a spurious new request submission on stream 3, storing pointer #2.
When the connection pool is cleaned up during shutdown, AddressSanitizer intercepts the illegal memory access:
=================================================================
==54128==ERROR: AddressSanitizer: heap-use-after-free on address 0x50c000000100 at pc 0x0000005a1e8c bp 0x7fff89ab10e0 sp 0x7fff89ab10d8
READ of size 8 at 0x50c000000100 thread T0
#0 0x5a1e8b in on_begin_headers lib/cf-h2.c:1373:42
#1 0x7f55b92e21b2 in nghttp2_session_on_header_received lib/nghttp2_session.c:4821
#2 0x7f55b92db943 in nghttp2_session_mem_recv lib/nghttp2_session.c:6120
#3 0x5a3e14 in h2_process_pending_input lib/cf-h2.c:487:14
#4 0x5a3962 in h2_progress_ingress lib/cf-h2.c:1865:12
#5 0x5a5611 in cf_h2_shutdown lib/cf-h2.c:2559:5
#6 0x582419 in Curl_conn_shutdown lib/cfilters.c:225:10
#7 0x5776d2 in Curl_cpool_destroy lib/conncache.c:247:7
#8 0x57291a in curl_share_cleanup lib/curl_share.c:370:7
#9 0x502b41 in main src/tool_main.c:329
0x50c000000100 is located 0 bytes inside of 5440-byte region [0x50c000000100,0x50c000001640)
freed by thread T0 here:
#0 0x4981ad in free
#1 0x57488f in Curl_close lib/url.c:303:5
#2 0x573bb2 in curl_easy_cleanup lib/easy.c:924:3
#3 0x5079a4 in del_per_transfer src/tool_operate.c:240:5
Ecosystem Impact: System Daemons, Cloud SDKs & Containers
Because libcurl is the default HTTP client engine across the computing industry, memory corruption flaws in its transfer pipeline have vast consequences:
| Ecosystem Consumer | Vulnerability Manifestation | Operational Threat Profile |
|---|---|---|
Command-Line curl |
Direct execution during script-based file uploads (curl -T <file>). |
Process crash or memory corruption when communicating with rogue endpoints. |
| Cloud Storage SDKs | AWS SDK for C++, Google Cloud Storage C++ Client, Azure Storage SDK. | High-speed multipart blob uploads over HTTP/2 trigger heap manipulation. |
| Container Runtimes | Docker and Podman image layer push routines via HTTP/2 container registries. | Host daemon instability or privilege escape if connecting to untrusted mirrors. |
| Version Control | Git HTTP smart transport (git push) over HTTP/2. |
Arbitrary code execution on developer machines pushing code to compromised Git remotes. |
Remediation & Upstream Patch Implementation
The curl development team resolved the defect by modifying the transfer wrap-up logic in lib/transfer.c. Whenever an HTTP/2 transfer terminates due to an RST_STREAM or stream failure, libcurl explicitly invokes Curl_req_abort_sending():
// Upstream patch in lib/transfer.c:
if(result != CURLE_AGAIN) {
/* Ensure unsent request buffers are purged before handling retries */
Curl_req_abort_sending(data);
goto out;
}
By purging req.sendbuf when the error occurs, subsequent invocations of Curl_req_done() encounter an empty buffer, preventing the spurious submission of stream 3 and ensuring no dangling references remain in nghttp2 user data structures.



