curl · Report #3636044
HTTP/2 PUSH_PROMISE header loss on OOM bypasses scheme validation (regression of 2e8c922a89)
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:
- Look at the push header callback in
lib/http2.c, around line 1490. Whencurl_maprintfreturns 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;
- Now look at the scheme validation in
set_transfer_url()at line 733. It depends on the:schemeheader 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.
-
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
:schemeheader 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.
-
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:schemeheader, 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)
- 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 returnsNGHTTP2_ERR_CALLBACK_FAILUREwhencurlx_reallocfails, but thecurl_maprintffailure 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.