HexBrain

curl · Report #3636044

HTTP/2 PUSH_PROMISE header loss on OOM bypasses scheme validation (regression of 2e8c922a89)

Informativestatus
mediumseverity
15upvotes
31 Mar 2026disclosed

How it was found

Summary:

In lib/http2.c:1490, when curl_maprintf fails due to memory pressure, the push promise header is silently dropped but the callback returns success. If the lost header is the :scheme pseudo-header, the security check at line 733 that blocks HTTPS pushes over insecure connections is skipped entirely. This re-enables the vulnerability that commit 2e8c922a89 was designed to fix.

Affected version

curl 8.20.0-DEV (Darwin) libcurl/8.20.0-DEV OpenSSL/3.6.1 zlib/1.2.12 brotli/1.2.0 zstd/1.5.7 libidn2/2.3.8 libssh2/1.11.1 nghttp2/1.66.0 OpenLDAP/2.4.28/Apple
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt mqtts pop3 pop3s rtsp scp sftp smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli HSTS HTTP2 HTTPS-proxy IDN IPv6 Largefile libz SSL threadsafe TLS-SRP UnixSockets zstd

All versions that contain the push scheme validation from commit 2e8c922a89 are affected.

Steps To Reproduce:

  1. Look at the push header callback in lib/http2.c, around line 1490. When curl_maprintf returns NULL, the header is silently dropped and the function returns 0 (success):
h = curl_maprintf("%s:%s", name, value);
if(h)
  stream->push_headers[stream->push_headers_used++] = h;
return 0;
  1. Now look at the scheme validation in set_transfer_url() at line 733. It depends on the :scheme header being present:
v = curl_pushheader_byname(hp, HTTP_PSEUDO_SCHEME);
if(v) {
  if(!via_ssl_conn) {
    const struct Curl_scheme *scheme = Curl_get_scheme(v);
    if(!scheme || (scheme->flags & PROTOPT_SSL)) {
      rc = 1;  /* reject secure scheme over insecure connection */
      goto fail;
    }
  }
}

If the :scheme header was lost in step 1, curl_pushheader_byname returns NULL, the if(v) branch is never entered, and the entire security check is skipped. The push proceeds with the default scheme inherited from the parent request.

  1. The attack works as follows:

    • A malicious HTTP/2 server communicates over a plaintext connection.
    • It sends a PUSH_PROMISE with many large headers to create memory pressure.
    • The :scheme header allocation fails, so it gets dropped silently.
    • The scheme validation is skipped.
    • The pushed resource is accepted as if it came from a secure origin.
  2. The attached PoC (poc_b1_h2_push_oom_bypass.c) simulates this exact flow. It collects push headers through the callback with a simulated OOM on the :scheme header, then runs the scheme validation. Build and run:

clang -fsanitize=address -o poc_b1 poc_b1_h2_push_oom_bypass.c && ./poc_b1

Output:

Normal HTTPS push over insecure connection → CORRECTLY BLOCKED
OOM drops :scheme header → ALLOWED (SECURITY BYPASS)
  1. Note that commit 5267bf52d8 ("http2: memory errors in the push callbacks are fatal") already established the precedent that allocation failures in push callbacks should abort the push. The code at line 1484 correctly returns NGHTTP2_ERR_CALLBACK_FAILURE when curlx_realloc fails, but the curl_maprintf failure at line 1490 does not follow this same pattern.

Suggested Fix

Return NGHTTP2_ERR_CALLBACK_FAILURE when curl_maprintf returns NULL instead of silently dropping the header:

h = curl_maprintf("%s:%s", name, value);
if(!h) {
  free_push_headers(stream);
  return NGHTTP2_ERR_CALLBACK_FAILURE;
}
stream->push_headers[stream->push_headers_used++] = h;
return 0;

We have a fix ready and can submit a PR at your convenience.

Impact

Under memory pressure, a malicious HTTP/2 server on a plaintext connection can push resources that bypass the scheme validation added in commit 2e8c922a89. The client processes the pushed content as if it came from a secure origin.

The practical risk depends on the client running in a memory-constrained environment, which is common in embedded systems and containers with strict memory limits. The attacker can increase the likelihood of OOM by sending many large push promise headers before the critical :scheme header.

No authentication is required. The push promise is processed as part of the HTTP/2 connection. The application must have push enabled via CURLMOPT_PUSHFUNCTION.

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