VYPR
High severityNVD Advisory· Published Jun 2, 2026· Updated Jun 2, 2026

CVE-2026-49754

CVE-2026-49754

Description

Allocation of Resources Without Limits or Throttling vulnerability in elixir-mint Mint allows attacker-controlled HTTP/2 servers to exhaust memory in a Mint client (HTTP/2 CONTINUATION flood).

When Mint's HTTP/2 receive path observes a HEADERS frame without the END_HEADERS flag, the unparsed header-block fragment is parked in conn.headers_being_processed, and every subsequent CONTINUATION frame on that stream is appended to the accumulator. Nothing in the receive path caps the accumulator: there is no per-stream size limit, no CONTINUATION frame-count limit, and max_header_list_size is only enforced on outgoing requests, never on inbound header blocks (its default is :infinity).

A malicious or compromised HTTP/2 server can stream an endless sequence of CONTINUATION frames (each up to the peer-advertised SETTINGS_MAX_FRAME_SIZE) and drive the client's iolist to arbitrary size, causing memory exhaustion and BEAM process death. A single connection to an attacker-controlled HTTP/2 endpoint is sufficient.

This issue affects mint: from 0.1.0 before 1.9.0.

Affected products

1

Patches

1
b662d127d302

Merge commit from fork

https://github.com/elixir-mint/mintEric Meadows-JönssonJun 2, 2026via body-scan
2 files changed · +123 8
  • lib/mint/http2.ex+56 8 modified
    @@ -156,6 +156,12 @@ defmodule Mint.HTTP2 do
       @default_max_frame_size 16_384
       @valid_max_frame_size_range @default_max_frame_size..16_777_215
     
    +  # Default cap on the size of an inbound header block. Advertised to the
    +  # server as SETTINGS_MAX_HEADER_LIST_SIZE and enforced while accumulating
    +  # HEADERS/CONTINUATION fragments, so that a server cannot exhaust client
    +  # memory by streaming an unbounded chain of CONTINUATION frames.
    +  @default_max_header_list_size 256 * 1024
    +
       @valid_client_settings [
         :max_concurrent_streams,
         :initial_window_size,
    @@ -235,7 +241,7 @@ defmodule Mint.HTTP2 do
         client_settings: %{
           max_concurrent_streams: 100,
           initial_window_size: @default_stream_window_size,
    -      max_header_list_size: :infinity,
    +      max_header_list_size: @default_max_header_list_size,
           max_frame_size: @default_max_frame_size,
           enable_push: true
         },
    @@ -297,7 +303,12 @@ defmodule Mint.HTTP2 do
         * `:max_frame_size` - corresponds to `SETTINGS_MAX_FRAME_SIZE`. Tells what is the
           maximum size of an HTTP/2 frame for the peer that sends this setting.
     
    -    * `:max_header_list_size` - corresponds to `SETTINGS_MAX_HEADER_LIST_SIZE`.
    +    * `:max_header_list_size` - corresponds to `SETTINGS_MAX_HEADER_LIST_SIZE`. For the
    +      client, this also bounds the size of an inbound header block (a HEADERS frame plus
    +      its trailing CONTINUATION frames): the connection is closed with a connection error
    +      if a server streams a header block larger than this value, which prevents a server
    +      from exhausting client memory with an unbounded chain of CONTINUATION frames.
    +      Defaults to `256 KB` for the client.
     
         * `:enable_connect_protocol` - corresponds to `SETTINGS_ENABLE_CONNECT_PROTOCOL`.
           Sets whether the client may invoke the extended connect protocol which is used to
    @@ -1140,7 +1151,9 @@ defmodule Mint.HTTP2 do
         client_settings_params = Keyword.get(opts, :client_settings, [])
     
         client_settings_params =
    -      Keyword.put_new(client_settings_params, :initial_window_size, @default_stream_window_size)
    +      client_settings_params
    +      |> Keyword.put_new(:initial_window_size, @default_stream_window_size)
    +      |> Keyword.put_new(:max_header_list_size, @default_max_header_list_size)
     
         validate_client_settings!(client_settings_params)
         # If the port is the default for the scheme, don't add it to the :authority pseudo-header
    @@ -1728,7 +1741,7 @@ defmodule Mint.HTTP2 do
           {nil, _frame} ->
             :ok
     
    -      {{stream_id, _, _}, continuation(stream_id: stream_id)} ->
    +      {{stream_id, _, _, _}, continuation(stream_id: stream_id)} ->
             :ok
     
           _other ->
    @@ -1906,7 +1919,7 @@ defmodule Mint.HTTP2 do
           decode_hbf_and_add_responses(conn, responses, hbf, stream, end_stream?)
         else
           callback = &decode_hbf_and_add_responses(&1, &2, &3, &4, end_stream?)
    -      conn = put_in(conn.headers_being_processed, {stream_id, hbf, callback})
    +      conn = start_headers_being_processed(conn, stream_id, hbf, callback)
           {conn, responses}
         end
       end
    @@ -2237,7 +2250,7 @@ defmodule Mint.HTTP2 do
           )
         else
           callback = &decode_push_promise_headers_and_add_response(&1, &2, &3, &4, promised_stream_id)
    -      conn = put_in(conn.headers_being_processed, {stream_id, hbf, callback})
    +      conn = start_headers_being_processed(conn, stream_id, hbf, callback)
           {conn, responses}
         end
       end
    @@ -2392,18 +2405,53 @@ defmodule Mint.HTTP2 do
           assert_stream_in_state(conn, stream, [:open, :half_closed_local, :reserved_remote])
         end
     
    -    {^stream_id, hbf_acc, callback} = conn.headers_being_processed
    +    {^stream_id, hbf_acc, callback, acc_size} = conn.headers_being_processed
     
         if flag_set?(flags, :continuation, :end_headers) do
           hbf = IO.iodata_to_binary([hbf_acc, hbf_chunk])
           conn = put_in(conn.headers_being_processed, nil)
           callback.(conn, responses, hbf, stream)
         else
    -      conn = put_in(conn.headers_being_processed, {stream_id, [hbf_acc, hbf_chunk], callback})
    +      new_size = acc_size + byte_size(hbf_chunk)
    +      conn = assert_header_block_within_max_size(conn, new_size)
    +
    +      conn =
    +        put_in(
    +          conn.headers_being_processed,
    +          {stream_id, [hbf_acc, hbf_chunk], callback, new_size}
    +        )
    +
           {conn, responses}
         end
       end
     
    +  defp start_headers_being_processed(conn, stream_id, hbf, callback) do
    +    hbf_size = byte_size(hbf)
    +    conn = assert_header_block_within_max_size(conn, hbf_size)
    +    put_in(conn.headers_being_processed, {stream_id, hbf, callback, hbf_size})
    +  end
    +
    +  # The header block accumulated from a HEADERS frame and its trailing
    +  # CONTINUATION frames is buffered (in compressed form) until END_HEADERS
    +  # arrives. A server can withhold END_HEADERS and stream CONTINUATION frames
    +  # indefinitely, so the buffered size is bounded by the locally advertised
    +  # SETTINGS_MAX_HEADER_LIST_SIZE. The compressed accumulator is never larger
    +  # than the uncompressed header list it decodes to, so this never rejects a
    +  # header block that fits within the advertised limit.
    +  defp assert_header_block_within_max_size(conn, size) do
    +    case conn.client_settings.max_header_list_size do
    +      :infinity ->
    +        conn
    +
    +      max_size when size > max_size ->
    +        debug_data = "header block exceeds SETTINGS_MAX_HEADER_LIST_SIZE of #{max_size} bytes"
    +        send_connection_error!(conn, :protocol_error, debug_data)
    +
    +      _max_size ->
    +        conn
    +    end
    +  end
    +
       ## General helpers
     
       defp send_connection_error!(conn, error_code, debug_data) do
    
  • test/mint/http2/conn_test.exs+67 0 modified
    @@ -963,6 +963,73 @@ defmodule Mint.HTTP2Test do
           end)
         end
     
    +    @tag connect_options: [client_settings: [max_header_list_size: 1_000]]
    +    test "a flood of CONTINUATION frames past max_header_list_size is a connection error",
    +         %{conn: conn} do
    +      {conn, _ref} = open_request(conn)
    +
    +      assert_recv_frames [headers(stream_id: stream_id)]
    +
    +      # Each CONTINUATION is individually under the limit, but together they
    +      # accumulate past the advertised SETTINGS_MAX_HEADER_LIST_SIZE. The client
    +      # must refuse to buffer the header block without bound rather than growing
    +      # `headers_being_processed` until it runs out of memory.
    +      chunk = :binary.copy(<<0>>, 400)
    +
    +      assert {:error, %HTTP2{} = conn, error, []} =
    +               stream_frames(conn, [
    +                 headers(stream_id: stream_id, hbf: "", flags: set_flags(:headers, [])),
    +                 continuation(
    +                   stream_id: stream_id,
    +                   hbf: chunk,
    +                   flags: set_flags(:continuation, [])
    +                 ),
    +                 continuation(
    +                   stream_id: stream_id,
    +                   hbf: chunk,
    +                   flags: set_flags(:continuation, [])
    +                 ),
    +                 continuation(
    +                   stream_id: stream_id,
    +                   hbf: chunk,
    +                   flags: set_flags(:continuation, [])
    +                 )
    +               ])
    +
    +      assert_http2_error error, {:protocol_error, debug_data}
    +      assert debug_data =~ "SETTINGS_MAX_HEADER_LIST_SIZE"
    +
    +      assert_recv_frames [goaway(error_code: :protocol_error)]
    +
    +      refute HTTP2.open?(conn)
    +    end
    +
    +    @tag connect_options: [client_settings: [max_header_list_size: 1_000]]
    +    test "a single oversized HEADERS fragment past max_header_list_size is a connection error",
    +         %{conn: conn} do
    +      {conn, _ref} = open_request(conn)
    +
    +      assert_recv_frames [headers(stream_id: stream_id)]
    +
    +      oversized_hbf = :binary.copy(<<0>>, 2_000)
    +
    +      assert {:error, %HTTP2{} = conn, error, []} =
    +               stream_frames(conn, [
    +                 headers(stream_id: stream_id, hbf: oversized_hbf, flags: set_flags(:headers, []))
    +               ])
    +
    +      assert_http2_error error, {:protocol_error, debug_data}
    +      assert debug_data =~ "SETTINGS_MAX_HEADER_LIST_SIZE"
    +
    +      assert_recv_frames [goaway(error_code: :protocol_error)]
    +
    +      refute HTTP2.open?(conn)
    +    end
    +
    +    test "client advertises a default SETTINGS_MAX_HEADER_LIST_SIZE", %{conn: conn} do
    +      assert HTTP2.get_client_setting(conn, :max_header_list_size) == 256 * 1024
    +    end
    +
         test ":authority pseudo-header includes port", %{conn: conn} do
           {conn, _ref} = open_request(conn)
     
    

Vulnerability mechanics

Root cause

"The HTTP/2 client did not enforce a limit on the size of accumulated header blocks received from a server."

Attack vector

An attacker controls an HTTP/2 server and sends a HEADERS frame without the END_HEADERS flag, followed by an unbounded sequence of CONTINUATION frames. Each CONTINUATION frame can be up to the peer-advertised SETTINGS_MAX_FRAME_SIZE. This causes the client's `conn.headers_being_processed` buffer to grow indefinitely, leading to memory exhaustion and a denial-of-service condition [ref_id=2].

Affected code

The vulnerability lies within the HTTP/2 receive path in Mint, specifically in how `handle_headers/3` and `handle_continuation/3` process incoming frames. The `conn.headers_being_processed` field was used to accumulate header fragments without a size cap [ref_id=2]. The fix modifies `lib/mint/http2.ex` to introduce `start_headers_being_processed/4` and `assert_header_block_within_max_size/2` to enforce the `SETTINGS_MAX_HEADER_LIST_SIZE`.

What the fix does

The patch introduces a limit to the size of accumulated header blocks received from a server. The client now enforces the `SETTINGS_MAX_HEADER_LIST_SIZE`, defaulting to 256 KB, by tracking the size of the compressed header block accumulator. If the accumulated size exceeds this limit, the connection is terminated with a PROTOCOL_ERROR [patch_id=4518409]. This prevents a malicious server from sending an unbounded chain of CONTINUATION frames and exhausting client memory.

Preconditions

  • inputThe client must connect to an HTTP/2 server that is either malicious or compromised.

Generated on Jun 2, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.