CVE-2026-8467
Description
Code Injection vulnerability in phenixdigital phoenix_storybook allows unauthenticated remote code execution via unsanitized attribute value interpolation in HEEx template generation.
The psb-assign WebSocket event handler in 'Elixir.PhoenixStorybook.Story.PlaygroundPreviewLive':handle_event/3 accepts arbitrary attribute names and values from unauthenticated clients. These values are passed to 'Elixir.PhoenixStorybook.Helpers.ExtraAssignsHelpers':handle_set_variation_assign/3, which stores them verbatim. When rendering, 'Elixir.PhoenixStorybook.Rendering.ComponentRenderer':attributes_markup/1 interpolates binary attribute values directly into a HEEx template string as name="" without escaping double quotes or HEEx expression delimiters. An attacker can supply a value containing a closing quote followed by a HEEx expression block (e.g. foo" injected={EXPR} bar="), which causes EXPR to be treated as an inline Elixir expression. The resulting template is compiled via EEx.compile_string/2 and executed via Code.eval_quoted_with_env/3 with full Kernel imports and no sandbox, giving the attacker arbitrary code execution on the server.
This issue affects phoenix_storybook from 0.5.0 before 1.1.0.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unauthenticated remote code execution via unsanitized attribute interpolation in phoenix_storybook's HEEx template rendering.
Vulnerability
A code injection vulnerability in phenixdigital/phoenix_storybook versions 0.5.0 up to but not including 1.1.0 allows unauthenticated remote code execution. The psb-assign WebSocket event handler in Elixir.PhoenixStorybook.Story.PlaygroundPreviewLive accepts arbitrary attribute names and values from unauthenticated clients [1][4]. These values are stored verbatim by ExtraAssignsHelpers.handle_set_variation_assign/3. During rendering, ComponentRenderer.attributes_markup/1 interpolates binary attribute values directly into a HEEx template string as name="" without escaping double quotes or HEEx expression delimiters [1][4]. An attacker can supply a value containing a closing quote followed by a HEEx expression block (e.g., foo" injected={EXPR} bar="), causing EXPR to be treated as an inline Elixir expression [2][3]. The resulting template is compiled via EEx.compile_string/2 and executed via Code.eval_quoted_with_env/3 with full Kernel imports and no sandbox [1][4].
Exploitation
An attacker needs only network access to the storybook endpoint; no authentication or special configuration is required [2][4]. The attacker identifies any story URL with a Playground tab, connects to the Phoenix LiveView WebSocket, joins the story's LiveView channel, and sends a psb-assign event with an attribute value that escapes the HEEx attribute context and embeds an Elixir expression (e.g., a System.cmd/2 call) [4]. The server evaluates the injected expression and returns its output in the rendered response [4].
Impact
Successful exploitation gives the attacker arbitrary code execution on the server with full Elixir Kernel access [1][2][3]. This results in complete compromise of confidentiality, integrity, and availability. The vulnerability is rated CVSS 9.5 (Critical) [2].
Mitigation
The vulnerability is fixed in phoenix_storybook version 1.1.0, released on 2026-05-20 [1][2][3][4]. The fix introduces input validation for attribute names and atoms in ExtraAssignsHelpers (commit 56ab8464) [1]. Users running affected versions should upgrade immediately. No workarounds are documented; if upgrading is not possible, the storybook endpoint should be restricted to trusted networks. This CVE is not listed in the CISA KEV catalog.
- Fix unsafe HEEx attribute generation in playground rendering · phenixdigital/phoenix_storybook@56ab846
- OSV - Open Source Vulnerabilities
- Unauthenticated remote code execution via HEEx template injection in phoenix_storybook playground
- Unauthenticated remote code execution via HEEx template injection in phoenix_storybook playground
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: >=0.5.0, <1.1.0
Patches
156ab8464d437Fix unsafe HEEx attribute generation in playground rendering
6 files changed · +268 −79
.credo.exs+1 −1 modified@@ -138,7 +138,6 @@ {Credo.Check.Warning.IoInspect, []}, {Credo.Check.Warning.OperationOnSameValues, []}, {Credo.Check.Warning.OperationWithConstantResult, []}, - {Credo.Check.Warning.RaiseInsideRescue, []}, {Credo.Check.Warning.SpecWithStruct, []}, {Credo.Check.Warning.WrongTestFileExtension, []}, {Credo.Check.Warning.UnusedEnumOperation, []}, @@ -189,6 +188,7 @@ {Credo.Check.Warning.LeakyEnvironment, []}, {Credo.Check.Warning.MapGetUnsafePass, []}, {Credo.Check.Warning.MixEnv, []}, + {Credo.Check.Warning.RaiseInsideRescue, []}, {Credo.Check.Warning.UnsafeToAtom, []} # {Credo.Check.Refactor.MapInto, []},
lib/phoenix_storybook/helpers/extra_assigns_helpers.ex+93 −12 modified@@ -5,6 +5,9 @@ defmodule PhoenixStorybook.ExtraAssignsHelpers do alias PhoenixStorybook.Stories.{Variation, VariationGroup} alias PhoenixStorybook.ThemeHelpers + @assign_attr_name_regex ~r/^[A-Za-z_:][A-Za-z0-9_:\.-]*[!?]?$/ + @reserved_assign_attrs ~w(__changed__ __struct__)a + def init_variation_extra_assigns(type, story) when type in [:component, :live_component] do extra_assigns = for %Variation{id: variation_id} <- story.variations(), @@ -48,15 +51,15 @@ defmodule PhoenixStorybook.ExtraAssignsHelpers do def handle_set_variation_assign(params, extra_assigns, story) do context = "assign" - variation_id = to_variation_id(params, context) + variation_id = to_variation_id(params, extra_assigns, context) params = Map.delete(params, "variation_id") variation_extra_assigns = to_variation_extra_assigns(extra_assigns, variation_id) variation_extra_assigns = for {attr, value} <- params, reduce: variation_extra_assigns do acc -> - attr = String.to_atom(attr) + attr = to_attr!(attr, story.attributes(), context) value = to_value(value, attr, story.attributes(), context) Map.put(acc, attr, value) end @@ -70,9 +73,9 @@ defmodule PhoenixStorybook.ExtraAssignsHelpers do attr = params |> Map.get_lazy("attr", fn -> raise "missing attr in #{context}" end) - |> String.to_atom() + |> to_attr!(story.attributes(), context) - variation_id = to_variation_id(params, context) + variation_id = to_variation_id(params, extra_assigns, context) variation_extra_assigns = to_variation_extra_assigns(extra_assigns, variation_id) current_value = Map.get(variation_extra_assigns, attr) check_type!(current_value, :boolean, context) @@ -95,24 +98,83 @@ defmodule PhoenixStorybook.ExtraAssignsHelpers do {variation_id, variation_extra_assigns} end - defp to_variation_id(%{"variation_id" => [group_id, variation_id]}, _ctx), - do: {String.to_atom(group_id), String.to_atom(variation_id)} + defp to_variation_id(%{"variation_id" => [group_id, variation_id]}, extra_assigns, context) do + find_variation_id!(extra_assigns, {group_id, variation_id}, context) + end + + defp to_variation_id(%{"variation_id" => variation_id}, extra_assigns, context) do + find_variation_id!(extra_assigns, {:single, variation_id}, context) + end + + defp to_variation_id(_, _extra_assigns, context), + do: raise("missing variation_id in #{context}") + + defp find_variation_id!(extra_assigns, expected_id, context) when is_map(extra_assigns) do + Enum.find(Map.keys(extra_assigns), &same_variation_id?(&1, expected_id)) || + raise("unknown variation_id in #{context}") + end + + defp find_variation_id!(_extra_assigns, _expected_id, context) do + raise("unknown variation_id in #{context}") + end - defp to_variation_id(%{"variation_id" => variation_id}, _ctx), - do: {:single, String.to_atom(variation_id)} + defp same_variation_id?({group_id, variation_id}, {expected_group_id, expected_variation_id}) do + to_string(group_id) == to_string(expected_group_id) && + to_string(variation_id) == to_string(expected_variation_id) + end - defp to_variation_id(_, context), do: raise("missing variation_id in #{context}") + defp same_variation_id?(_variation_id, _expected_id), do: false defp to_variation_extra_assigns(extra_assigns, id = {_group_id, _variation_id}) do - Map.get(extra_assigns, id) + Map.fetch!(extra_assigns, id) + end + + defp to_attr!(attr, attributes, context) when is_atom(attr) do + attr + |> Atom.to_string() + |> validate_attr_name!(context) + + validate_attr!(attr, attributes, context) + end + + defp to_attr!(attr, attributes, context) when is_binary(attr) do + validate_attr_name!(attr, context) + + attr = + declared_attr_id(attr, attributes) || + existing_attr_atom!(attr, context) + + validate_attr!(attr, attributes, context) + end + + defp to_attr!(attr, _attributes, context) do + raise(RuntimeError, "invalid attribute name in #{context}: #{inspect(attr)}") + end + + defp validate_attr_name!(attr, context) do + unless Regex.match?(@assign_attr_name_regex, attr) do + raise(RuntimeError, "invalid attribute name in #{context}: #{attr}") + end + end + + defp validate_attr!(attr, _attributes, context) when attr in @reserved_assign_attrs do + raise(RuntimeError, "invalid attribute name in #{context}: #{attr}") + end + + defp validate_attr!(attr, _attributes, _context), do: attr + + defp existing_attr_atom!(attr, context) do + String.to_existing_atom(attr) + rescue + ArgumentError -> raise(RuntimeError, "unknown attribute in #{context}: #{attr}") end defp to_value("nil", _attr_id, _attributes, _context), do: nil defp to_value(val, attr_id, attributes, context) when is_binary(val) do case declared_attr_type(attr_id, attributes) do - :atom -> val |> String.to_atom() |> check_type!(:atom, context) - :boolean -> val |> String.to_atom() |> check_type!(:boolean, context) + :atom -> val |> existing_value_atom!(context) |> check_type!(:atom, context) + :boolean -> val |> parse_boolean!(context) |> check_type!(:boolean, context) :integer -> val |> Integer.parse() |> check_type!(:integer, context) :float -> val |> Float.parse() |> check_type!(:float, context) _ -> val @@ -133,6 +195,25 @@ defmodule PhoenixStorybook.ExtraAssignsHelpers do end end + defp declared_attr_id(attr, attributes) do + Enum.find_value(attributes, fn %Attr{id: id} -> + if to_string(id) == attr, do: id + end) + end + + defp existing_value_atom!(val, context) do + String.to_existing_atom(val) + rescue + ArgumentError -> raise(RuntimeError, "unknown atom value in #{context}: #{val}") + end + + defp parse_boolean!("true", _context), do: true + defp parse_boolean!("false", _context), do: false + + defp parse_boolean!(val, context) do + raise(RuntimeError, "type mismatch in #{context}: #{val} is not a boolean") + end + defp check_type!(nil, _type, _context), do: nil defp check_type!(atom, :atom, _context) when is_atom(atom), do: atom defp check_type!(boolean, :boolean, _context) when is_boolean(boolean), do: boolean
lib/phoenix_storybook/rendering/component_renderer.ex+108 −64 modified@@ -23,95 +23,124 @@ defmodule PhoenixStorybook.Rendering.ComponentRenderer do end def render(fun_or_mod, context = %RenderingContext{}) do - heex = + {heex, attrs} = cond do TemplateHelpers.variation_template?(context.template) -> - for variation = %RenderingVariation{} <- context.variations, into: "" do - template_heex( - fun_or_mod, - {context.group_id, variation.id}, - context.template, - variation, - context.options[:playground_topic] - ) - end + context.variations + |> Enum.with_index() + |> Enum.map_reduce(%{}, fn {variation = %RenderingVariation{}, index}, attrs -> + {heex, runtime_attrs} = + template_heex( + fun_or_mod, + {context.group_id, variation.id}, + context.template, + variation, + index, + context.options[:playground_topic] + ) + + {heex, Map.put(attrs, index, runtime_attrs)} + end) + |> then(fn {heex, attrs} -> {Enum.join(heex, ""), attrs} end) TemplateHelpers.variation_group_template?(context.template) -> - heex = - for variation = %RenderingVariation{} <- context.variations, into: "" do + {components_heex, attrs} = + context.variations + |> Enum.with_index() + |> Enum.map_reduce(%{}, fn {variation = %RenderingVariation{}, index}, attrs -> extra_attributes = extract_placeholder_attributes( context.template, variation.id, context.options[:playground_topic] ) - component_heex( - fun_or_mod, - variation.attributes, - variation.let, - variation.slots, - extra_attributes - ) - end + {heex, runtime_attrs} = + component_heex( + fun_or_mod, + variation.attributes, + variation.let, + variation.slots, + index, + extra_attributes + ) + + {heex, Map.put(attrs, index, runtime_attrs)} + end) + + heex = + context.template + |> TemplateHelpers.set_variation_dom_id(context.dom_id) + |> TemplateHelpers.set_js_push_variation_id(context.group_id) + |> TemplateHelpers.replace_template_variation_group(Enum.join(components_heex, "")) - context.template - |> TemplateHelpers.set_variation_dom_id(context.dom_id) - |> TemplateHelpers.set_js_push_variation_id(context.group_id) - |> TemplateHelpers.replace_template_variation_group(heex) + {heex, attrs} true -> - context.template - |> TemplateHelpers.set_variation_dom_id(context.dom_id) - |> TemplateHelpers.set_js_push_variation_id(context.group_id) + {context.template + |> TemplateHelpers.set_variation_dom_id(context.dom_id) + |> TemplateHelpers.set_js_push_variation_id(context.group_id), %{}} end - render_component_heex(fun_or_mod, heex, context.options) + render_component_heex(fun_or_mod, heex, context.options, attrs) end - defp component_heex(fun, assigns, _let, [], extra_attrs) when is_function(fun) do - """ - <.#{function_name(fun)} #{attributes_markup(assigns)} #{extra_attrs}/> - """ + defp component_heex(fun, assigns, _let, [], index, extra_attrs) when is_function(fun) do + {attributes_markup, runtime_attrs} = attributes_markup(assigns, index) + + {""" + <.#{function_name(fun)} #{attributes_markup} #{extra_attrs}/> + """, runtime_attrs} end - defp component_heex(fun, assigns, let, slots, extra_attrs) when is_function(fun) do - """ - <.#{function_name(fun)} #{let_markup(let)} #{attributes_markup(assigns)} #{extra_attrs}> - #{slots} - </.#{function_name(fun)}> - """ + defp component_heex(fun, assigns, let, slots, index, extra_attrs) when is_function(fun) do + {attributes_markup, runtime_attrs} = attributes_markup(assigns, index) + + {""" + <.#{function_name(fun)} #{let_markup(let)} #{attributes_markup} #{extra_attrs}> + #{slots} + </.#{function_name(fun)}> + """, runtime_attrs} end - defp component_heex(module, assigns, _let, [], extra_attrs) when is_atom(module) do - """ - <.live_component module={#{inspect(module)}} #{attributes_markup(assigns)} #{extra_attrs}/> - """ + defp component_heex(module, assigns, _let, [], index, extra_attrs) when is_atom(module) do + {attributes_markup, runtime_attrs} = attributes_markup(assigns, index, [:module]) + + {""" + <.live_component module={#{inspect(module)}} #{attributes_markup} #{extra_attrs}/> + """, runtime_attrs} end - defp component_heex(module, assigns, let, slots, extra_attrs) when is_atom(module) do - """ - <.live_component module={#{inspect(module)}} #{let_markup(let)} #{attributes_markup(assigns)} #{extra_attrs}> - #{slots} - </.live_component> - """ + defp component_heex(module, assigns, let, slots, index, extra_attrs) when is_atom(module) do + {attributes_markup, runtime_attrs} = attributes_markup(assigns, index, [:module]) + + {""" + <.live_component module={#{inspect(module)}} #{let_markup(let)} #{attributes_markup} #{extra_attrs}> + #{slots} + </.live_component> + """, runtime_attrs} end defp template_heex( fun_or_mod, variation_id, template, %RenderingVariation{dom_id: dom_id, let: let, slots: slots, attributes: attributes}, + index, playground_topic ) do extra_attributes = extract_placeholder_attributes(template, variation_id, playground_topic) - template - |> TemplateHelpers.set_variation_dom_id(dom_id) - |> TemplateHelpers.set_js_push_variation_id(variation_id) - |> TemplateHelpers.replace_template_variation( - component_heex(fun_or_mod, attributes, let, slots, extra_attributes) - ) + {component_heex, runtime_attrs} = + component_heex(fun_or_mod, attributes, let, slots, index, extra_attributes) + + heex = + template + |> TemplateHelpers.set_variation_dom_id(dom_id) + |> TemplateHelpers.set_js_push_variation_id(variation_id) + |> TemplateHelpers.replace_template_variation(component_heex) + + {heex, runtime_attrs} end defp extract_placeholder_attributes(template, _variation_id, _topic = nil) do @@ -125,20 +154,35 @@ defmodule PhoenixStorybook.Rendering.ComponentRenderer do defp let_markup(nil), do: "" defp let_markup(let), do: ":let={#{to_string(let)}}" - defp attributes_markup(attributes) do - Enum.map_join(attributes, " ", fn - {name, {:eval, val}} -> + defp attributes_markup(attributes, index, reserved_attrs \\ []) do + {eval_attrs, runtime_attrs} = + attributes + |> Enum.reject(fn {name, _val} -> name in reserved_attrs end) + |> Enum.split_with(fn + {_name, {:eval, _val}} -> true + _attr -> false + end) + + eval_attrs_markup = + Enum.map_join(eval_attrs, " ", fn {name, {:eval, val}} -> ~s|#{name}={#{val}}| + end) + + markup = + ["{Map.fetch!(@psb_variation_attrs, #{index})}", eval_attrs_markup] + |> Enum.reject(&(&1 == "")) + |> Enum.join(" ") + + {markup, Map.new(runtime_attrs)} + end - {name, val} when is_binary(val) -> - ~s|#{name}="#{val}"| + defp render_component_heex(fun_or_mod, heex, opts, attrs) do + assigns = %{psb_variation_attrs: attrs} - {name, val} -> - ~s|#{name}={#{inspect(val, structs: false, limit: :infinity, printable_limit: :infinity)}}| - end) + eval_component_heex(fun_or_mod, heex, opts, assigns) end - defp render_component_heex(fun_or_mod, heex, opts) do + defp eval_component_heex(fun_or_mod, heex, opts, assigns) do quoted_code = EEx.compile_string(heex, engine: TagEngine, @@ -161,7 +205,7 @@ defmodule PhoenixStorybook.Rendering.ComponentRenderer do {evaluated, _, _} = Code.eval_quoted_with_env( quoted_code, - [assigns: %{}], + [assigns: assigns], env )
test/phoenix_storybook/helpers/extra_assigns_helpers_test.exs+31 −0 modified@@ -97,6 +97,14 @@ defmodule PhoenixStorybook.ExtraAssignsHelpersTest do story ) end + + assert_raise RuntimeError, ~r/unknown atom value in assign/, fn -> + handle_set_variation_assign( + %{"variation_id" => "variation_id", "atom" => "psb_unknown_atom_value"}, + %{{:single, :variation_id} => %{}}, + story + ) + end end test "with nil typed attributes", %{story: story} do @@ -134,6 +142,29 @@ defmodule PhoenixStorybook.ExtraAssignsHelpersTest do handle_set_variation_assign(%{}, %{}, story) end end + + test "rejects unknown variation ids", %{story: story} do + assert_raise RuntimeError, ~r/unknown variation_id in assign/, fn -> + handle_set_variation_assign( + %{"variation_id" => "unknown", "attribute" => "foo"}, + %{{:single, :variation_id} => %{}}, + story + ) + end + end + + test "rejects invalid attribute names", %{story: story} do + assert_raise RuntimeError, ~r/invalid attribute name in assign/, fn -> + handle_set_variation_assign( + %{ + "variation_id" => "variation_id", + ~s|attribute" injected={send(self(), :rce)} bar| => "foo" + }, + %{{:single, :variation_id} => %{}}, + story + ) + end + end end describe "handle_toggle_variation_assign/3" do
test/phoenix_storybook/live/playground_live_test.exs+19 −0 modified@@ -30,6 +30,25 @@ defmodule PhoenixStorybook.PlaygroundLiveTest do assert view |> element("#psb-playground-preview-live") |> render() =~ "component: world" end + test "playground psb-assign values are not evaluated as HEEx", %{conn: conn} do + {:ok, view, _html} = live(conn, "/storybook/component?tab=playground") + assert [playground_preview_view] = live_children(view) + + process_name = :"playground_rce_#{System.unique_integer([:positive])}" + Process.register(self(), process_name) + + payload = + ~s|safe" injected={send(Process.whereis(#{inspect(process_name)}), :rce)} bar="| + + render_hook(playground_preview_view, "psb-assign", %{ + "variation_id" => "hello", + "label" => payload + }) + + refute_received :rce + Process.unregister(process_name) + end + test "renders playground code a simple component", %{conn: conn} do {:ok, view, _html} = live(conn, "/storybook/component?tab=playground") view |> element("a", "Code") |> render_click()
test/phoenix_storybook/rendering/component_renderer_test.exs+16 −2 modified@@ -162,6 +162,20 @@ defmodule PhoenixStorybook.Rendering.ComponentRendererTest do assert render_variation(component, :with_eval) |> rendered_to_string() =~ ~s|index_i: 25| end + test "renders binary attributes without evaluating injected HEEx", %{component: component} do + process_name = :"component_renderer_rce_#{System.unique_integer([:positive])}" + Process.register(self(), process_name) + + payload = + ~s|safe" injected={send(Process.whereis(#{inspect(process_name)}), :rce)} bar="| + + assert render_variation(component, :hello, %{hello: %{label: payload}}) + |> rendered_to_string() =~ "safe" + + refute_received :rce + Process.unregister(process_name) + end + test "it should not crash with a very large binary in a map" do defmodule LargeBinaryStory do use PhoenixStorybook.Story, :component @@ -231,11 +245,11 @@ defmodule PhoenixStorybook.Rendering.ComponentRendererTest do end end - defp render_variation(story, variation_id) do + defp render_variation(story, variation_id, extra_attributes \\ %{}) do variation = Enum.find(story.variations(), &(&1.id == variation_id)) TreeStorybook - |> RenderingContext.build(story, variation, %{}) + |> RenderingContext.build(story, variation, extra_attributes) |> ComponentRenderer.render() end end
Vulnerability mechanics
Root cause
"Unsanitized binary attribute values are interpolated directly into a HEEx template string without escaping double quotes or HEEx expression delimiters, allowing an attacker to break out of the attribute context and inject arbitrary Elixir code."
Attack vector
An unauthenticated attacker sends a crafted `psb-assign` WebSocket event to the playground LiveView with a binary attribute value containing a closing double-quote followed by a HEEx expression block (e.g. `foo" injected={EXPR} bar="`). The `handle_set_variation_assign/3` function [patch_id=852378] previously stored this value verbatim without validation. When `attributes_markup/1` in `ComponentRenderer` interpolated the value as `name="<val>"` into a dynamically compiled HEEx template, the injected expression was treated as an inline Elixir expression. The template was compiled via `EEx.compile_string/2` and executed via `Code.eval_quoted_with_env/3` with full Kernel imports and no sandbox, giving the attacker arbitrary code execution on the server.
Affected code
The vulnerability is in `lib/phoenix_storybook/rendering/component_renderer.ex`, specifically the `attributes_markup/1` function which interpolated binary attribute values directly into a HEEx template as `name="<val>"` without escaping. The entry point is `lib/phoenix_storybook/helpers/extra_assigns_helpers.ex`, where `handle_set_variation_assign/3` accepted arbitrary attribute names and values from unauthenticated WebSocket clients and stored them verbatim. The `PlaygroundPreviewLive` LiveView in `Elixir.PhoenixStorybook.Story.PlaygroundPreviewLive` dispatches the `psb-assign` event to this handler.
What the fix does
The patch [patch_id=852378] fundamentally changes how runtime attributes are rendered. Instead of interpolating binary values directly into the HEEx template string as `name="<val>"`, the new `attributes_markup/2` function splits attributes into eval-type and runtime attributes. Runtime attributes are collected into a map and passed as a single `@psb_variation_attrs` assign, which is spread into the component render via `{Map.fetch!(@psb_variation_attrs, index)}`. This means binary values are never placed inside double-quoted attribute slots in the template string, so injected quotes and HEEx delimiters cannot break out of the attribute context. Additionally, `ExtraAssignsHelpers` now validates attribute names against a strict regex (`@assign_attr_name_regex`), rejects reserved atoms, uses `String.to_existing_atom/1` instead of `String.to_atom/1` to prevent atom table exhaustion, and validates variation IDs against the known set of variations rather than blindly converting them to atoms.
Preconditions
- networkAttacker must be able to reach the Phoenix Storybook HTTP endpoint (typically served on the application's web port).
- inputAttacker must send a WebSocket message with event name 'psb-assign' containing a 'variation_id' and at least one attribute name/value pair where the value contains a crafted HEEx injection payload.
Generated on May 20, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4News mentions
0No linked articles in our index yet.