VYPR
High severityNVD Advisory· Published May 1, 2026· Updated May 5, 2026

CVE-2026-39804

CVE-2026-39804

Description

Allocation of Resources Without Limits or Throttling vulnerability in mtrudel bandit allows unauthenticated remote denial of service via memory exhaustion when WebSocket permessage-deflate compression is enabled.

'Elixir.Bandit.WebSocket.PerMessageDeflate':inflate/2 in lib/bandit/websocket/permessage_deflate.ex calls :zlib.inflate/2 with no output-size cap, then materializes the entire decompressed payload as a single binary via IO.iodata_to_binary/1. The websocket_options.max_frame_size option only bounds the on-the-wire (compressed) frame size, not the decompressed output. A high-ratio compressed frame (e.g. uniform data at ~1024:1 ratio) can stay well under any wire-size limit while forcing GiB-scale heap allocations in the connection process before any application code runs.

An unauthenticated attacker who can open a WebSocket connection can send a single such frame to exhaust the BEAM node's memory and trigger an OOM kill.

This vulnerability requires both Bandit's server-level websocket_options.compress and the per-upgrade compress: true option passed to WebSockAdapter.upgrade/4 to be enabled. Stock Phoenix and LiveView applications are not affected as they default to compress: false.

This issue affects bandit: from 0.5.9 before 1.11.0.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
banditHex
>= 0.5.8, < 1.11.01.11.0

Affected products

1

Patches

1
8156921a51e6

Merge commit from fork

https://github.com/mtrudel/banditMat TrudelMay 1, 2026via ghsa
4 files changed · +90 11
  • lib/bandit.ex+5 1 modified
    @@ -202,13 +202,17 @@ defmodule Bandit do
       * `deflate_options`: A keyword list of options to set on the deflate library when using the
         per-message deflate extension. A complete list can be found at `t:deflate_options/0`.
         `window_bits` is currently ignored and left to negotiation.
    +  * `max_inflate_ratio`: The maximum allowable ratio to allow decompression of received WebSocket
    +    messages. Intended to prevent 'inflate bomb' attacks where a tiny deflated messages inflates to
    +    a massive one. Defaults to `25` representing a 25:1 allowable inflation ratio.
       """
       @type websocket_options :: [
               {:enabled, boolean()}
               | {:max_frame_size, pos_integer()}
               | {:validate_text_frames, boolean()}
               | {:compress, boolean()}
               | {:deflate_options, deflate_options()}
    +          | {:max_inflate_ratio, pos_integer()}
             ]
     
       @typedoc """
    @@ -245,7 +249,7 @@ defmodule Bandit do
       @http_keys ~w(compress response_encodings deflate_options zstd_options log_exceptions_with_status_codes log_protocol_errors log_client_closures)a
       @http_1_keys ~w(enabled max_request_line_length max_header_length max_header_count max_requests clear_process_dict gc_every_n_keepalive_requests log_unknown_messages)a
       @http_2_keys ~w(enabled max_header_block_size max_requests max_reset_stream_rate sendfile_chunk_size default_local_settings)a
    -  @websocket_keys ~w(enabled max_frame_size validate_text_frames compress deflate_options primitive_ops_module)a
    +  @websocket_keys ~w(enabled max_frame_size validate_text_frames compress deflate_options max_inflate_ratio primitive_ops_module)a
       @thousand_island_keys ThousandIsland.ServerConfig.__struct__()
                             |> Map.from_struct()
                             |> Map.keys()
    
  • lib/bandit/websocket/connection.ex+3 0 modified
    @@ -308,6 +308,9 @@ defmodule Bandit.WebSocket.Connection do
           {:error, :no_compress} ->
             do_error(1002, "Received unexpected compressed frame (RFC6455§5.2)", socket, connection)
     
    +      {:error, :too_much_inflation} ->
    +        do_error(1009, "Received compressed frame inflating too much", socket, connection)
    +
           {:error, _reason} ->
             do_error(1007, "Inflation error", socket, connection)
         end
    
  • lib/bandit/websocket/permessage_deflate.ex+46 10 modified
    @@ -9,17 +9,19 @@ defmodule Bandit.WebSocket.PerMessageDeflate do
               server_max_window_bits: 8..15,
               client_max_window_bits: 8..15,
               inflate_context: :zlib.zstream(),
    -          deflate_context: :zlib.zstream()
    +          deflate_context: :zlib.zstream(),
    +          max_inflate_ratio: integer()
             }
     
       defstruct server_no_context_takeover: false,
                 client_no_context_takeover: false,
                 server_max_window_bits: 15,
                 client_max_window_bits: 15,
                 inflate_context: nil,
    -            deflate_context: nil
    +            deflate_context: nil,
    +            max_inflate_ratio: nil
     
    -  @valid_params ~w[server_no_context_takeover client_no_context_takeover server_max_window_bits client_max_window_bits]
    +  @valid_params ~w[server_no_context_takeover client_no_context_takeover server_max_window_bits client_max_window_bits max_inflate_ratio]
     
       def negotiate(requested_extensions, opts) do
         :proplists.get_all_values("permessage-deflate", requested_extensions)
    @@ -86,6 +88,7 @@ defmodule Bandit.WebSocket.PerMessageDeflate do
         instance = struct(__MODULE__, params)
         inflate_context = :zlib.open()
         :ok = :zlib.inflateInit(inflate_context, fix_bits(-instance.client_max_window_bits))
    +
         deflate_context = :zlib.open()
         deflate_opts = Keyword.get(opts, :deflate_options, [])
     
    @@ -99,7 +102,14 @@ defmodule Bandit.WebSocket.PerMessageDeflate do
             Keyword.get(deflate_opts, :strategy, :default)
           )
     
    -    %{instance | inflate_context: inflate_context, deflate_context: deflate_context}
    +    max_inflate_ratio = Keyword.get(opts, :max_inflate_ratio, 25)
    +
    +    %{
    +      instance
    +      | inflate_context: inflate_context,
    +        deflate_context: deflate_context,
    +        max_inflate_ratio: max_inflate_ratio
    +    }
       end
     
       # https://www.erlang.org/doc/man/zlib.html#deflateInit-6
    @@ -109,19 +119,45 @@ defmodule Bandit.WebSocket.PerMessageDeflate do
       # Note that we pass back the context to the caller even though it is unmodified locally
     
       def inflate(data, %__MODULE__{} = context) do
    -    inflated_data =
    -      context.inflate_context
    -      |> :zlib.inflate(<<data::binary, 0x00, 0x00, 0xFF, 0xFF>>)
    -      |> IO.iodata_to_binary()
    +    safe_inflate(
    +      context.inflate_context,
    +      :zlib.safeInflate(context.inflate_context, <<data::binary, 0x00, 0x00, 0xFF, 0xFF>>),
    +      [],
    +      byte_size(data) * context.max_inflate_ratio
    +    )
    +    |> case do
    +      {:ok, inflated_iodata, inflate_context} ->
    +        if context.client_no_context_takeover, do: :zlib.inflateReset(context.inflate_context)
    +        {:ok, IO.iodata_to_binary(inflated_iodata), %{context | inflate_context: inflate_context}}
     
    -    if context.client_no_context_takeover, do: :zlib.inflateReset(context.inflate_context)
    -    {:ok, inflated_data, context}
    +      {:error, reason} ->
    +        {:error, reason}
    +    end
       rescue
         e -> {:error, "Error encountered #{inspect(e)}"}
       end
     
       def inflate(_data, nil), do: {:error, :no_compress}
     
    +  defp safe_inflate(inflate_context, {:continue, deflated}, buffer, bytes_remaining)
    +       when bytes_remaining > 0 do
    +    safe_inflate(
    +      inflate_context,
    +      :zlib.safeInflate(inflate_context, <<>>),
    +      [buffer | deflated],
    +      bytes_remaining - IO.iodata_length(deflated)
    +    )
    +  end
    +
    +  defp safe_inflate(_inflate_context, {:continue, _deflated}, _buffer, bytes_remaining)
    +       when bytes_remaining <= 0 do
    +    {:error, :too_much_inflation}
    +  end
    +
    +  defp safe_inflate(inflate_context, {:finished, deflated}, buffer, _bytes_remaining) do
    +    {:ok, [buffer | deflated], inflate_context}
    +  end
    +
       def deflate(data, %__MODULE__{} = context) do
         deflated_data =
           context.deflate_context
    
  • test/bandit/websocket/protocol_test.exs+36 0 modified
    @@ -239,6 +239,42 @@ defmodule WebSocketProtocolTest do
           assert SimpleWebSocketClient.recv_pong_frame(client) == {:ok, "OK"}
           assert SimpleWebSocketClient.recv_ping_frame(client) == {:ok, "OK"}
         end
    +
    +    test "server sends a 1009 on an overly compressed frame", context do
    +      output =
    +        capture_log(fn ->
    +          zstream = :zlib.open()
    +          :ok = :zlib.deflateInit(zstream, :default, :deflated, -15, 8, :default)
    +
    +          deflated_chunks =
    +            Enum.map(
    +              1..1_000_000,
    +              fn _ -> :zlib.deflate(zstream, "aaaaaaaaaa", :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
    +
    +          client = SimpleWebSocketClient.tcp_client(context)
    +          SimpleWebSocketClient.http1_handshake(client, TerminateWebSock, [], true)
    +          SimpleWebSocketClient.send_text_frame(client, payload, 0xC)
    +
    +          # Get the error that terminate saw, to ensure we're closing for the expected reason
    +          assert_receive {:error, "Received compressed frame inflating too much"}, 500
    +
    +          # Validate that the server has started the shutdown handshake from RFC6455§7.1.2
    +          assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, <<1009::16>>}
    +
    +          # Verify that the server didn't send any extraneous frames
    +          assert SimpleWebSocketClient.connection_closed_for_reading?(client)
    +          Process.sleep(500)
    +        end)
    +
    +      assert output =~ "Received compressed frame inflating too much"
    +    end
       end
     
       describe "ping frames" do
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.