curl · Report #3630310
HTTP/2 server push accepts a non-authoritative :scheme=https over cleartext h2c, enabling HTTPS cache-key poisoning
How it was found
Summary:
I found that libcurl 8.19.0 accepts an HTTP/2 pushed stream on a cleartext h2c connection even when the server sends :scheme=https in PUSH_PROMISE. In lib/http2.c, set_transfer_url() builds the pushed handle URL from the server-supplied :scheme, :authority, and :path, but PUSH_PROMISE validation only checks :authority and does not reject a non-authoritative pushed https origin. The accepted pushed handle is then exposed to the application and processed on the existing connection, and CURLINFO_EFFECTIVE_URL later returns the pushed URL from data->state.url. As a result, a cleartext h2c server can cause a pushed transfer to appear as https://... to the application even though the bytes arrived over cleartext HTTP/2. In the attached cache PoC, a libcurl-based application that opts into push stores attacker-controlled cleartext content under a trusted HTTPS cache key and later serves it from cache without a network fetch.
Affected version
Official curl 8.19.0 source release on Linux/Kali, built locally with HTTP/2 enabled.
curl 8.19.0 (Linux) libcurl/8.19.0 OpenSSL/3.5.5 zlib/1.3.1 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 OpenLDAP/2.6.10
Release-Date: 2026-03-11
Relevant source locations in the official 8.19.0 source tree:
lib/http2.c:716set_transfer_url()starts building the pushed URLlib/http2.c:728:schemeis copied into the pushed URLlib/http2.c:1424PUSH_PROMISEvalidation checks only:authoritylib/http2.c:819the pushed handle is exposed to the application callbacklib/http2.c:839the accepted pushed handle is added for processinglib/multi.c:1676the pushed handle is attached to the existing connectionlib/multi.c:934data->conn = connlib/getinfo.c:91CURLINFO_EFFECTIVE_URLreturnsdata->state.url
Steps To Reproduce:
- Attach and extract the provided PoC bundle.
- Run the cache-poisoning server:
python3 h2_push_cache_server.py 18080 - Run the cache-poisoning client:
./h2_push_cache_client_8190 http://trusted.example:18080/ trusted.example:18080:127.0.0.1 - Observe that the main request is cleartext HTTP/2 to
http://trusted.example:18080/, but the push callback reports:effective_url=https://trusted.example:18080/pushed - Observe the cache evidence:
https://trusted.example:18080/pushed => ATTACKER-CONTROLLED-CLEARTEXT-PUSH - Observe the final poisoned-cache reuse:
POISONED CACHE HIT for trusted HTTPS key https://trusted.example:18080/pushedandTRUSTED PROCESSING of cached body: ATTACKER-CONTROLLED-CLEARTEXT-PUSH
The attached bundle also contains a simpler PoC that demonstrates the same primitive without the cache layer.
Impact
Summary:
By accepting a non-authoritative pushed HTTPS origin over cleartext h2c and surfacing it as an effective HTTPS URL, libcurl enables applications that opt into HTTP/2 push and cache pushed responses by CURLINFO_EFFECTIVE_URL to store attacker-controlled cleartext content under a trusted HTTPS cache key and later serve it without a network fetch. This is a concrete integrity impact, not just cosmetic metadata confusion. Server push is opt-in, but once enabled libcurl should enforce the HTTP/2 authority/origin boundary before exposing the pushed transfer to the application. I am requesting High severity because the attached PoC demonstrates concrete cache poisoning of a trusted HTTPS cache key, although I understand a more conservative Medium assessment.