Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions priv/posts/security/20260526200323_atom-exhaustion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{
"title": "Atom Exhaustion Is Not a Footgun. It's One Third of Our CVEs.",
"authors": ["Jonatan Männchen"],
"slug": "atom-exhaustion",
"category": "security",
"tags": ["security", "atom-exhaustion", "vulnerability", "dos"],
"datetime": "2026-05-26T20:03:23.779126Z"
}
---
Over one third of CVEs published by the EEF CNA are caused by atom exhaustion,
a denial-of-service vulnerability that is well understood, often preventable,
and still showing up in production Erlang and Elixir applications.
---

**35.8% of CVEs published by the Erlang Ecosystem Foundation CNA fall into the
category of uncontrolled resource consumption.** In the BEAM ecosystem, a large
share of those are caused by one recurring issue: atom exhaustion. You can find
the current distribution on the EEF CNA's [Common Weaknesses][cna-common-weaknesses]
page.

Atom exhaustion is a denial-of-service vulnerability. Atoms are not garbage
collected and are stored in a global atom table, and once it fills up, the VM
crashes. Creating atoms from non-finite values, especially user-supplied input,
is therefore a latent DoS waiting to happen.

This is not limited to obvious calls such as `binary_to_atom/1`,
`list_to_atom/1`, `String.to_atom/1`, or `List.to_atom/1`. Some dangerous
patterns are less obvious:

```erlang
% Erlang: dynamic atom creation through interpolation
list_to_atom("field_" ++ UserInput)
```

```elixir
# Elixir: decoding JSON with atom keys
Jason.decode(json, keys: :atoms)
```

```elixir
# Elixir: dynamic atom creation through interpolation
:"field_#{user_input}"
```

What makes this class of vulnerability persistent is not carelessness. It often
appears in code where the input was assumed to be controlled or finite. URI
schemes are a good example: it may feel like there are only a few schemes to
handle, but if the value comes from external input, the set is no longer
guaranteed to be finite.

> Creating atoms from input is unsafe unless the set of possible values is
> finite, known, and enforced.

The safest approach is to avoid creating new atoms at runtime entirely. Prefer
explicit lookup tables when the accepted values are known:

```erlang
% Erlang
case Scheme of
<<"http">> -> http;
<<"https">> -> https;
_ -> error
end
```

When a lookup table is not practical, use the safer existing-atom variants,
which will raise an error instead of creating a new atom:

```erlang
% Erlang
binary_to_existing_atom(Value)
list_to_existing_atom(Value)
```

```elixir
# Elixir
String.to_existing_atom(value)
List.to_existing_atom(value)
```

Linters can help catch these patterns before they become vulnerabilities. For
Elixir projects, consider enabling Credo's
[`Credo.Check.Warning.UnsafeToAtom`][credo-unsafe-to-atom], which flags unsafe
calls to `String.to_atom/1`, `List.to_atom/1`, `Module.concat/1,2`, and
`Jason.decode/2` with `keys: :atoms`. The check is disabled by default.

If you maintain an Erlang or Elixir project, search your codebase for atom
creation from binaries, strings, JSON keys, URI components, headers, and
configuration values. This is one of the easiest vulnerability classes to fix
before it becomes a CVE.

For more detailed guidance, see the EEF Security Working Group's
[guide on preventing atom exhaustion][atom-exhaustion-guide].

[cna-common-weaknesses]: https://cna.erlef.org/common-weaknesses
[atom-exhaustion-guide]: https://security.erlef.org/secure_coding_and_deployment_hardening/atom_exhaustion
[credo-unsafe-to-atom]: https://hexdocs.pm/credo/Credo.Check.Warning.UnsafeToAtom.html
Loading