HexBrain

curl · Report #3734020

HTTP/2 proxy CONNECT tunnel unbounded 1xx chain (missing Curl_bump_headersize cap in cf-h2-proxy.c)

Informativestatus
7upvotes
19 May 2026disclosed

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.

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