HTTP/2 server push accepts a non-authoritative :scheme=https over cleartext h2c, enabling HTTPS cache-key poisoning

Disclosed: 2026-03-29 16:44:13 By xkiluar To curl
High
Vulnerability Details
## 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:716` `set_transfer_url()` starts building the pushed URL - `lib/http2.c:728` `:scheme` is copied into the pushed URL - `lib/http2.c:1424` `PUSH_PROMISE` validation checks only `:authority` - `lib/http2.c:819` the pushed handle is exposed to the application callback - `lib/http2.c:839` the accepted pushed handle is added for processing - `lib/multi.c:1676` the pushed handle is attached to the existing connection - `lib/multi.c:934` `data->conn = conn` - `lib/getinfo.c:91` `CURLINFO_EFFECTIVE_URL` returns `data->state.url` ## Steps To Reproduce: 1. Attach and extract the provided PoC bundle. 2. Run the cache-poisoning server: `python3 h2_push_cache_server.py 18080` 3. Run the cache-poisoning client: `./h2_push_cache_client_8190 http://trusted.example:18080/ trusted.example:18080:127.0.0.1` 4. 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` 5. Observe the cache evidence: `https://trusted.example:18080/pushed => ATTACKER-CONTROLLED-CLEARTEXT-PUSH` 6. Observe the final poisoned-cache reuse: `POISONED CACHE HIT for trusted HTTPS key https://trusted.example:18080/pushed` and `TRUSTED 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.
Actions
View on HackerOne
Report Stats
  • Report ID: 3630310
  • State: Closed
  • Substate: informative
  • Upvotes: 1
Share this report