HexBrain

curl · Report #3630310

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

Informativestatus
highseverity
26upvotes
29 Mar 2026disclosed

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: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.

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