curl · Report #3973213
54: Rejected HTTP/2 push destroys MIME callback state still used by parent (use-after-free)
How it was found
Summary
push_promise() in lib/http2.c duplicates the parent easy handle before validating the pushed URL. When validation fails (e.g. unsupported scheme), discard_newhandle() destroys the duplicate. For callback-backed MIME uploads, Curl_mime_duppart() copies the same readfunc, freefunc, and arg pointer, so destroying the duplicate calls freefunc(arg) while the parent still holds and reads from that same arg. The CURLMOPT_PUSHFUNCTION callback is never invoked for the rejected push, so the application has no opportunity to add a reference. A malicious HTTP/2 server can therefore trigger a heap-use-after-free in the parent's MIME read callback.
Root cause
In push_promise() (lib/http2.c), the ordering is:
h2_duphandle()→curl_easy_duphandle()→dupset()→Curl_mime_duppart()— duplicates the MIME tree, copying callbackarg/freefuncas-is forMIMEKIND_CALLBACKparts.set_transfer_url()— validates the:scheme/:authority/:pathfrom the PUSH_PROMISE headers.- On validation failure:
discard_newhandle()→Curl_close()→Curl_mime_cleanpart()→cleanup_part_content()→freefunc(arg). - The push callback at line 827 is never reached; the application cannot account for this hidden duplicate.
curl_mime_data_cb(3) documents that duplicated handles share arg and call freefunc for each copy, and CURLMOPT_PUSHFUNCTION(3) documents that the pushed handle is duplicated from the parent. An application's defense is to add a reference in the push callback — but that callback is skipped for URL-invalid pushes.
Affected versions
curl 7.77.0 through 8.21.0, plus affected 8.22.0 prerelease/development versions.
Impact
A malicious HTTP/2 server triggers a heap-use-after-free in an application that uses CURLMOPT_PUSHFUNCTION with a callback-backed MIME upload. The demonstrated result is memory corruption and crash. Exploitability beyond denial of service was not established. The required combination of opt-in features (server push + MIME callback uploads with freefunc ownership) limits the attack surface.
Reproduction
- Build an affected curl release, starting with 7.77.0, with HTTP/2 server-push support and AddressSanitizer enabled.
- Start an HTTP/2 server that accepts a request and can send a crafted
PUSH_PROMISEwhile the parent transfer remains active. - Create a libcurl multi handle and register a
CURLMOPT_PUSHFUNCTIONcallback. - Allocate a heap-backed upload-state object with an explicit reference count initialized for the parent transfer.
- Create a callback-backed MIME part with
curl_mime_data_cb(), using the allocated state as its callback argument and supplying read, seek, and free callbacks. - Make the MIME read callback pause on its first invocation so that the parent easy handle remains alive while the server push is processed.
- In the push callback, increment the shared MIME state's reference count for the duplicated pushed handle, then deny the push. This models the ownership accounting available through the documented API.
- Add the parent easy handle to the multi handle and begin the MIME upload over HTTP/2.
- After the parent pauses, have the server send a syntactically formed
PUSH_PROMISEwhose promised URL curl rejects before invoking the application push callback, such as one with an unsupported scheme. - Observe that curl first duplicates the parent easy handle and its callback-backed MIME tree, including the same callback argument and free function.
- Confirm that URL validation then fails, the hidden duplicate is destroyed, and its MIME cleanup invokes the shared free callback.
- Confirm that the application push callback was never called and therefore had no opportunity to add the duplicate's reference.
- Resume the parent upload after the rejected push has been processed.
- Observe the vulnerable result: the parent's MIME read callback accesses the state freed during rejected-push cleanup, and AddressSanitizer reports a heap use-after-free.
- As a control, repeat the test with a valid same-origin push URL that reaches
CURLMOPT_PUSHFUNCTION. - Confirm that the callback increments the reference count before denying the push, duplicate cleanup releases only that reference, and the parent upload resumes without a sanitizer error.
- Confirm a fix by validating and constructing the promised URL before duplicating the parent easy handle.
- Repeat the invalid-push test and verify that no MIME duplicate is created, the shared free callback is not called prematurely, and the parent resumes safely.
Expected result
A push rejected before the application callback must not create and destroy a hidden duplicate that owns shared MIME callback state. The parent must retain valid ownership until its own cleanup.
Vulnerable result
Curl duplicates the parent before validating the pushed URL. Rejecting that URL destroys the duplicate and frees callback state still referenced by the live parent, causing a heap use-after-free when the upload resumes.
Suggested fix
Validate and construct the promised URL before duplicating the parent handle. This way a rejected push never creates a MIME callback clone hidden from the application.
Impact
A malicious HTTP/2 server can send a rejected push promise that causes curl to free heap-backed MIME callback state while the parent upload still references it. Resuming the upload triggers memory corruption and can reliably terminate the application; more severe exploitation may be possible depending on allocator behavior and surrounding application state, but code execution was not demonstrated.