curl · Report #3734020
HTTP/2 proxy CONNECT tunnel unbounded 1xx chain (missing Curl_bump_headersize cap in cf-h2-proxy.c)
How it was found
A malicious HTTPS-on-HTTP/2 proxy can grow a libcurl client's resident set without bound during the CONNECT phase by streaming 1xx informational responses. The CVE-2023-38039 cap (MAX_HTTP_RESP_HEADER_SIZE, 300 KiB, enforced through Curl_bump_headersize) is not applied on the HTTP/2 proxy path. The HTTP/2 proxy filter was introduced in commit 4ae2d9f24d (2023-04-06, "proxy: http2 proxy tunnel implementation") and the :status → new-http_resp → prev-chain pattern with no depth cap has been there since.
The short version: every :status pseudo-header on the proxy CONNECT stream goes through proxy_h2_on_header (lib/cf-h2-proxy.c:558-577), which allocates a fresh struct http_resp per status and chains it via resp->prev. The tunnel state machine only considers the CONNECT response "final" when it sees a non-1xx status (lib/cf-h2-proxy.c:506-509), so a peer that streams 1xx forever keeps the accumulator alive forever. There is no chain-depth cap, and Curl_bump_headersize is never called from cf-h2-proxy.c.
Curl_http_resp_make (lib/http.c:4973-4998) init's both headers and trailers dynhds with DYN_HTTP_REQUEST = 1 MiB. That's a per-entry ceiling — it does not chain. An attacker that fills each entry just under the per-dynhds byte cap and then sends a fresh :status to start a new http_resp gets ~1 MiB per chain link, no upper bound. Even the bare-:status spammer mode allocates ~1.4 KiB per frame against ~3-5 bytes on the wire (compressed HPACK), so roughly 1000× amplification.
Reproducer setup:
openssl req -x509 -newkey rsa:2048 -keyout /tmp/proxy.key -out /tmp/proxy.crt -days 1 -nodes -subj "/CN=localhost" 2>/dev/null && pip3 install --quiet --break-system-packages h2
...
$ BURST=50000 bash poc.sh
time_s rss_mib
0 1.0
1 18.5
2 26.6
3 34.6
4 42.7
5 50.8
6 58.7
7 66.7
8 74.6
9 82.6
10 90.7
11 98.7
12 106.8
13 114.8
14 122.8
16 130.5
17 138.6
18 146.6
19 154.6
20 162.6
21 170.7
22 178.8
23 186.7
24 194.6
25 202.7
26 210.6
27 218.5
28 226.5
29 234.5
30 242.4
31 250.5
32 258.5
33 266.4
34 274.4
35 282.3
36 290.3
37 298.3
38 306.1
39 314.1
40 322.1
41 330.1
42 338.2
43 346.2
44 354.1
45 362.2
46 370.2
47 378.1
48 386.1
49 394.1
50 402.0
51 409.9
52 417.9
53 425.9
..
it just grows forever. rss = Resident Set Size — the amount of physical RAM curl is actually using at that moment.
(If you take a look at the script (I don't recommend it), a minimum variant with no padding at all (PAD_HEADERS=0, just :status:103 repeated 5000 times) still moves RSS from 10 to 17 MiB (+7 MiB / 5000 frames ≈ 1.4 KiB per :status frame), against ~3-5 bytes on the wire (HPACK-indexed :status:103) — ~1000× amplification in spammer mode)
Cleanest fix is to call Curl_bump_headersize from proxy_h2_on_header the way cf-h1-proxy.c:381 already does for the HTTP/1 proxy — same 300 KiB cap, just wasn't wired into the h2 path. Capping the :status chain depth in proxy_h2_on_header (or discarding the previous link if it was 1xx, since the CONNECT state machine only reads the most recent status) would also work.
Cheers, Joshua
Impact
A malicious HTTP/2 proxy can grow a libcurl client's memory without bound during the CONNECT phase by streaming 1xx responses indefinitely, achieving ~1000× amplification (3–5 bytes on the wire per ~1.4 KiB allocated), with no defence other than --max-time.