HTTP/2 PUSH_PROMISE header loss on OOM bypasses scheme validation (regression of 2e8c922a89)
Medium
Vulnerability Details
## 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):
```c
h = curl_maprintf("%s:%s", name, value);
if(h)
stream->push_headers[stream->push_headers_used++] = h;
return 0;
```
2. Now look at the scheme validation in `set_transfer_url()` at line 733. It depends on the `:scheme` header being present:
```c
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.
3. 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.
4. 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:
```bash
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)
```
5. 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:
```c
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`.
Actions
View on HackerOneReport Stats
- Report ID: 3636044
- State: Closed
- Substate: informative
- Upvotes: 1