VYPR
Medium severityNVD Advisory· Published Jun 11, 2026· Updated Jun 11, 2026

CVE-2026-53423

CVE-2026-53423

Description

Membrane MP4 plugin converts each 4-byte box name to a permanent atom; a crafted 8MB file with ~1.1M distinct names exhausts the BEAM atom table, crashing the entire node.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Membrane MP4 plugin converts each 4-byte box name to a permanent atom; a crafted 8MB file with ~1.1M distinct names exhausts the BEAM atom table, crashing the entire node.

Vulnerability

The Membrane MP4 plugin (membrane_mp4_plugin) contains an allocation-of-resources vulnerability in the MP4 box header parser [1][3]. The function Membrane.MP4.Container.Header.parse_box_name/1 in lib/membrane_mp4/container/header.ex calls String.to_atom/1 on each 4-byte box name encountered while parsing an MP4 container [1][2][4]. BEAM atoms are never garbage-collected, so each unique attacker-controlled name becomes a permanent allocation. Affected versions are from 0.3.0 up to but not including 0.36.7 [1][3].

Exploitation

An unauthenticated attacker with the ability to supply a crafted MP4 file to any system using membrane_mp4_plugin can trigger the vulnerability [1][4]. No authentication or special privilege is required. A payload of approximately 8 MB containing roughly 1.1 million box headers with distinct 4-byte names (each header is 8 bytes: 4-byte size and 4-byte name) causes the parser to call String.to_atom/1 for each unique name [1][4]. Any code path that invokes Membrane.MP4.Container.parse/1 (or its bang variant) on the attacker-controlled bytes will consume a unique atom for every box [4].

Impact

A successful attack exhausts the BEAM atom table, whose default ceiling is around 1,048,576 atoms [1][3]. Since atoms are never reclaimed, the node reaches the limit permanently and aborts, terminating all Elixir/Erlang applications running on that BEAM instance [1][4]. The CIA outcome is a denial of service (availability loss) with no impact on confidentiality or integrity; the entire runtime crashes, taking down all hosted services [1].

Mitigation

The issue is fixed in version 0.36.7 [2][3]. The fix replaces String.to_atom/1 with String.to_existing_atom/1 and returns an :unknown_box error for unrecognized box names instead of creating a new atom [2][4]. Users should immediately update their dependency to {:membrane_mp4_plugin, "~> 0.36.7"} or later [2]. No workaround is available for older versions; upgrading is the sole recommended mitigation [4].

AI Insight generated on Jun 11, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
56373d1ddc86

Use String.to_exisiting_atom() instead of String.to_atom() and ignore box if it's name is not recognized. Release v0.36.7. (#135)

8 files changed · +84 33
  • lib/membrane_mp4/container/header.ex+23 8 modified
    @@ -25,7 +25,10 @@ defmodule Membrane.MP4.Container.Header do
     
       Returns the `t:t/0` and the leftover data.
       """
    -  @spec parse(binary()) :: {:ok, t, leftover :: binary()} | {:error, :not_enough_data}
    +  @spec parse(binary()) ::
    +          {:ok, t, leftover :: binary()}
    +          | {:error, :not_enough_data}
    +          | {:error, {:unknown_box, binary(), non_neg_integer(), non_neg_integer()}}
       def parse(
             <<compact_size::integer-size(@compact_size_size)-unit(8), name::binary-size(@name_size),
               rest::binary>>
    @@ -46,17 +49,29 @@ defmodule Membrane.MP4.Container.Header do
               {header_size, size - header_size, rest}
           end
     
    -    {:ok,
    -     %__MODULE__{
    -       name: parse_box_name(name),
    -       content_size: content_size,
    -       header_size: header_size
    -     }, rest}
    +    case parse_box_name(name) do
    +      {:ok, atom_name} ->
    +        {:ok,
    +         %__MODULE__{
    +           name: atom_name,
    +           content_size: content_size,
    +           header_size: header_size
    +         }, rest}
    +
    +      :error ->
    +        {:error, {:unknown_box, name, header_size, content_size}}
    +    end
       end
     
       def parse(_data), do: {:error, :not_enough_data}
     
       defp parse_box_name(name) do
    -    name |> String.trim_trailing(" ") |> String.to_atom()
    +    trimmed_name = String.trim_trailing(name)
    +
    +    try do
    +      {:ok, String.to_existing_atom(trimmed_name)}
    +    rescue
    +      ArgumentError -> :error
    +    end
       end
     end
    
  • lib/membrane_mp4/container/parse_helper.ex+18 3 modified
    @@ -17,10 +17,10 @@ defmodule Membrane.MP4.Container.ParseHelper do
       end
     
       def parse_boxes(data, schema, context, acc) do
    -    withl header_content:
    +    withl header:
                 {:ok, %{name: name, content_size: content_size, header_size: header_size}, rest} <-
                   Header.parse(data),
    -          header_content: <<content::binary-size(content_size), data::binary>> <- rest,
    +          content: <<content::binary-size(content_size), data::binary>> <- rest,
               do: box_schema = schema[name],
               known?: true <- box_schema && not box_schema.black_box?,
               try:
    @@ -31,7 +31,22 @@ defmodule Membrane.MP4.Container.ParseHelper do
           box = %{fields: fields, children: children, size: content_size, header_size: header_size}
           parse_boxes(data, schema, context, [{name, box} | acc])
         else
    -      header_content: _error ->
    +      header: {:error, {:unknown_box, name, header_size, content_size}} ->
    +        case data do
    +          <<_header::binary-size(header_size), content::binary-size(content_size),
    +            remaining::binary>> ->
    +            box = %{name: name, content: content, size: content_size, header_size: header_size}
    +            parse_boxes(remaining, schema, context, [{:unknown, box} | acc])
    +
    +          _not_enough ->
    +            {:ok, Enum.reverse(acc), data, context}
    +        end
    +
    +      header: _error ->
    +        # more data needed
    +        {:ok, Enum.reverse(acc), data, context}
    +
    +      content: _error ->
             # more data needed
             {:ok, Enum.reverse(acc), data, context}
     
    
  • lib/membrane_mp4/container/serialize_helper.ex+8 0 modified
    @@ -25,6 +25,14 @@ defmodule Membrane.MP4.Container.SerializeHelper do
         end
       end
     
    +  defp serialize_box(:unknown, %{name: name, content: content}, _schema, context) do
    +    header =
    +      <<@box_header_size + byte_size(content)::integer-size(@box_size_size)-unit(8),
    +        name::binary>>
    +
    +    {{:ok, [header, content]}, context}
    +  end
    +
       defp serialize_box(box_name, %{content: content}, _schema, context) do
         header = serialize_header(box_name, byte_size(content))
         {{:ok, [header, content]}, context}
    
  • lib/membrane_mp4/demuxer/cmaf/engine.ex+1 1 modified
    @@ -21,7 +21,7 @@ defmodule Membrane.MP4.Demuxer.CMAF.Engine do
         :pending_emsg
       ]
     
    -  @ignored_boxes [:free, :skip]
    +  @ignored_boxes [:free, :skip, :unknown]
     
       @opaque t() :: %__MODULE__{}
     
    
  • lib/membrane_mp4/demuxer/isom/engine.ex+18 13 modified
    @@ -139,19 +139,24 @@ defmodule Membrane.MP4.Demuxer.ISOM.Engine do
           state.provide_data_cb.(state.cursor, @max_header_size, state.provider_state)
     
         state = put_in(state.provider_state, provider_state)
    -    {:ok, header, _rest} = Container.Header.parse(data)
    -
    -    if header.name == box_name do
    -      state =
    -        update_in(
    -          state.box_positions,
    -          &Map.put(&1, box_name, {state.cursor, header.header_size, header.content_size})
    -        )
    -
    -      %{state | cursor: 0}
    -    else
    -      update_in(state.cursor, &(&1 + header.header_size + header.content_size))
    -      |> find_box(box_name)
    +
    +    case Container.Header.parse(data) do
    +      {:ok, %{name: ^box_name} = header, _rest} ->
    +        state =
    +          update_in(
    +            state.box_positions,
    +            &Map.put(&1, box_name, {state.cursor, header.header_size, header.content_size})
    +          )
    +
    +        %{state | cursor: 0}
    +
    +      {:ok, header, _rest} ->
    +        update_in(state.cursor, &(&1 + header.header_size + header.content_size))
    +        |> find_box(box_name)
    +
    +      {:error, {:unknown_box, _name, header_size, content_size}} ->
    +        update_in(state.cursor, &(&1 + header_size + content_size))
    +        |> find_box(box_name)
         end
       end
     
    
  • mix.exs+1 1 modified
    @@ -1,7 +1,7 @@
     defmodule Membrane.MP4.Plugin.MixProject do
       use Mix.Project
     
    -  @version "0.36.6"
    +  @version "0.36.7"
       @github_url "https://github.com/membraneframework/membrane_mp4_plugin"
     
       def project do
    
  • README.md+1 1 modified
    @@ -12,7 +12,7 @@ The package can be installed by adding `membrane_mp4_plugin` to your list of dep
     ```elixir
     defp deps do
       [
    -    {:membrane_mp4_plugin, "~> 0.36.6"}
    +    {:membrane_mp4_plugin, "~> 0.36.7"}
       ]
     end
     ```
    
  • test/membrane_mp4/container_test.exs+14 6 modified
    @@ -52,13 +52,21 @@ defmodule Membrane.MP4.ContainerTest do
         assert Keyword.has_key?(boxes, :skip)
       end
     
    -  test "unknown box" do
    -    <<size::4-binary, "styp", rest::binary>> =
    -      @cmaf_fixtures |> Path.join("ref_audio_segment1.m4s") |> File.read!()
    +  test "box with unknown name is stored as :unknown and round-trips through serialization" do
    +    unknown_name = "zz9z"
    +    unknown_content = <<0, 1, 2, 3>>
     
    -    data = <<size::4-binary, "abcd", rest::binary>>
    -    assert {boxes, <<>>} = data |> Container.parse!()
    -    assert boxes |> Container.serialize!() == data
    +    unknown_box =
    +      <<8 + byte_size(unknown_content)::32, unknown_name::binary, unknown_content::binary>>
    +
    +    known_data = @cmaf_fixtures |> Path.join("ref_audio_segment1.m4s") |> File.read!()
    +    data = unknown_box <> known_data
    +
    +    {boxes, <<>>} = Container.parse!(data)
    +
    +    assert_raise ArgumentError, fn -> String.to_existing_atom(unknown_name) end
    +    assert [{:unknown, %{name: ^unknown_name, content: ^unknown_content}} | _rest] = boxes
    +    assert Container.serialize!(boxes) == data
       end
     
       test "parse error" do
    

Vulnerability mechanics

Root cause

"Missing input validation in MP4 box header parser uses String.to_atom/1 to intern every 4-byte box name as a BEAM atom without an allow-list, causing permanent atom table allocations that are never garbage-collected."

Attack vector

An attacker crafts an MP4 binary containing roughly 1.1 million minimal box headers (each 8 bytes: 4-byte size + 4-byte unique name) for a total of ~8 MB [ref_id=2]. When any code path calls `Membrane.MP4.Container.parse/1` or its bang variant on this payload, `parse_box_name/1` in `lib/membrane_mp4/container/header.ex` passes each unique 4-byte name through `String.to_atom/1`, which unconditionally creates a new BEAM atom [ref_id=2]. BEAM atoms are never garbage-collected, so each unique name is a permanent allocation; once the runtime ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it [ref_id=2]. No authentication or user interaction is required beyond delivering the file to any processing path that calls the parser [ref_id=2].

Affected code

The vulnerable code is `parse_box_name/1` in `lib/membrane_mp4/container/header.ex`, which calls `String.to_atom/1` on the 4-byte box name without any allow-list validation [patch_id=5593506]. This function is invoked by `Membrane.MP4.Container.Header.parse/1` for every box encountered while walking the input stream [ref_id=2]. The patch also touches `lib/membrane_mp4/container/parse_helper.ex`, `lib/membrane_mp4/container/serialize_helper.ex`, `lib/membrane_mp4/demuxer/isom/engine.ex`, and `lib/membrane_mp4/demuxer/cmaf/engine.ex` to propagate the new error handling [patch_id=5593506].

What the fix does

The patch replaces `String.to_atom/1` with `String.to_existing_atom/1` inside `parse_box_name/1` in `lib/membrane_mp4/container/header.ex` [patch_id=5593506]. When the trimmed box name does not already exist as an atom, the `ArgumentError` is rescued and `:error` is returned instead of creating a new atom [patch_id=5593506]. Downstream callers in `parse_helper.ex`, `serialize_helper.ex`, and the ISOM/CMAF demuxer engines now handle the `{:error, {:unknown_box, name, header_size, content_size}}` tuple by treating the box as `:unknown` and skipping it, rather than crashing or creating a new atom [patch_id=5593506]. The `:unknown` atom is added to the `@ignored_boxes` list in the CMAF demuxer engine so unknown boxes are safely skipped during demuxing [patch_id=5593506].

Preconditions

  • inputAttacker must be able to supply a crafted MP4 binary to any code path that calls Membrane.MP4.Container.parse/1 or its bang variant
  • networkNo authentication or network position required — file delivery to the processing pipeline is sufficient
  • configThe target BEAM node must have the default atom table ceiling (~1,048,576 atoms) or lower

Reproduction

Generate an MP4-shaped payload of roughly 1.1 million minimal box headers, each 8 bytes long (4-byte size of 8, followed by a unique 4-byte ASCII name produced by enumerating combinations in the printable range). Concatenate them into a single binary (~8 MB total). Call `Membrane.MP4.Container.parse!/1` on the binary inside any process running under the target BEAM node. The node aborts once the atom table ceiling is reached. [ref_id=2]

Generated on Jun 11, 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.