Atom table exhaustion via unrecognized URL schemes in hackney
Description
Allocation of Resources Without Limits or Throttling vulnerability in benoitc hackney allows Flooding. The URL parser in src/hackney_url.erl converts every unrecognized URL scheme to a permanent BEAM atom via binary_to_atom/2. BEAM atoms are never garbage-collected and the atom table defaults to a hard limit of 1,048,576 entries. An attacker who can supply URLs with attacker-chosen scheme prefixes — directly as request targets, as configured webhook URLs, or via Location headers followed during redirects — can exhaust the atom table and crash the entire BEAM VM with system_limit.
This issue affects hackney: from 2.0.0 before 4.0.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Unrecognized URL schemes in hackney are converted to permanent BEAM atoms, allowing an attacker to exhaust the atom table and crash the VM.
Vulnerability
The HTTP client library hackney (Erlang/OTP) contains a resource-exhaustion vulnerability in its URL parser, hackney_url:parse_url/1 located in src/hackney_url.erl [1][2]. The parser extracts the URL scheme (the part before ://), validates it loosely against RFC 3986, lowercases it, and converts it to a BEAM atom via binary_to_atom(SchemeLower, utf8). BEAM atoms are never garbage-collected and the atom table has a hard default limit of 1,048,576 entries. An attacker can supply URLs with arbitrarily many distinct scheme prefixes—directly as request targets, as webhook or callback URLs, or via Location headers during redirect following—causing each unique scheme to mint a new permanent atom. This affects hackney from version 2.0.0 before 4.0.1 [1][3].
Exploitation
An attacker requires no authentication or special network position beyond the ability to supply URLs to a running hackney process. For example, an attacker can send a series of HTTP requests with URLs like aaaa://x, aaab://x, etc., or control a server that responds with redirect Location headers containing unique schemes. Each such URL adds a new atom to the BEAM VM's atom table. The validation restricts the scheme to a 19-character alphabetic string, but the number of distinct valid strings is astronomically large, far exceeding the atom limit [2]. No user interaction is required beyond the victim's application consuming the attacker-supplied URLs.
Impact
When the atom table reaches its hard limit, the BEAM VM emits a system_limit error and terminates entirely. This constitutes a denial-of-service (CWE-770, CAPEC-125 Flooding) with high availability impact. The CVSS v4.0 score is 8.7 (HIGH) with vector AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N [1]. The entire VM crashes, requiring a full restart to recover. No confidentiality or integrity impact is directly caused, but the service disruption is total.
Mitigation
The vulnerability is fixed in hackney version 4.0.1, released alongside the advisory on 2026-05-25 [2][3]. The fix, visible in commit 31f6f0e27e096ad88743dfded4f030a3ee74972e, changes the scheme field from an atom to a binary and replaces binary_to_atom/2 with binary_to_existing_atom/2; unrecognized schemes are now returned as binaries and rejected by the dispatcher without consuming atom table slots [4]. Users should upgrade to version 4.0.1 or later. No workaround is available in prior versions. The CVE is not listed in the CISA Known Exploited Vulnerabilities catalog at time of publication.
AI Insight generated on May 25, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
131f6f0e27e09fix(security): prevent atom-table exhaustion via URL scheme (GHSA-9653)
3 files changed · +30 −3
include/hackney_lib.hrl+1 −1 modified@@ -1,6 +1,6 @@ -record(hackney_url, { transport :: atom(), - scheme :: atom(), + scheme :: atom() | binary(), netloc = <<>> :: binary(), raw_path :: binary() | undefined, path = <<>> :: binary() | undefined | nil,
src/hackney_url.erl+10 −2 modified@@ -64,8 +64,16 @@ parse_url(URL) -> case is_valid_scheme(Scheme) of true -> SchemeLower = hackney_bstr:to_lower(Scheme), - SchemeAtom = binary_to_atom(SchemeLower, utf8), - parse_url(Rest, #hackney_url{transport=undefined, scheme=SchemeAtom}); + %% GHSA-9653: never mint a fresh atom for an attacker-supplied + %% scheme. `binary_to_existing_atom` only matches schemes that + %% have already been registered (i.e. ones the runtime, this + %% module, or the caller has explicitly seen). Anything else + %% stays as a binary; the dispatcher rejects it as + %% unsupported_scheme either way. + SchemeRef = try binary_to_existing_atom(SchemeLower, utf8) + catch error:badarg -> SchemeLower + end, + parse_url(Rest, #hackney_url{transport=undefined, scheme=SchemeRef}); false -> %% Not a valid scheme, treat as path-like URL parse_url(URL, #hackney_url{transport=hackney_tcp, scheme=http})
test/hackney_url_tests.erl+19 −0 modified@@ -592,3 +592,22 @@ unsupported_scheme_test_() -> ?assertEqual(Expected#hackney_url.transport, R#hackney_url.transport), ?assertEqual(Expected#hackney_url.scheme, R#hackney_url.scheme) end} || {V, Expected} <- Tests]. + +%% GHSA-9653: parse_url must not mint a fresh atom for every attacker-supplied +%% scheme. binary_to_existing_atom keeps the atom table bounded; unknown +%% schemes are returned as the lowercased binary instead. +parse_url_does_not_intern_attacker_schemes_test() -> + Before = erlang:system_info(atom_count), + Schemes = [iolist_to_binary(["zzghsa9653-", integer_to_list(I)]) + || I <- lists:seq(1, 200)], + lists:foreach( + fun(S) -> + U = hackney_url:parse_url(<<S/binary, "://host/p">>), + Scheme = U#hackney_url.scheme, + ?assert(is_binary(Scheme) orelse is_atom(Scheme)), + ?assertEqual(undefined, U#hackney_url.transport) + end, Schemes), + Delta = erlang:system_info(atom_count) - Before, + %% Allow a small slack for any unrelated atoms minted during the loop; + %% a regression would mint ~200 (one per unique scheme). + ?assert(Delta < 20).
Vulnerability mechanics
Root cause
"Missing throttling in URL parser: `binary_to_atom/2` creates a permanent BEAM atom for every unrecognized URL scheme, allowing atom-table exhaustion."
Attack vector
An attacker supplies URLs with attacker-chosen scheme prefixes — directly as request targets, as configured webhook URLs, or via `Location` headers followed during redirects [ref_id=1]. Each unique scheme prefix is converted to a permanent BEAM atom via `binary_to_atom/2` in `parse_url/1` [patch_id=2473695]. Because BEAM atoms are never garbage-collected and the atom table has a hard limit of 1,048,576 entries, an attacker can exhaust the atom table and crash the entire BEAM VM with `system_limit` [ref_id=1]. The most dangerous path is redirect following: an attacker-controlled server can serve a sequence of redirects each with a fresh unique scheme, driving the atom count monotonically upward [ref_id=1].
Affected code
The vulnerability resides in `src/hackney_url.erl` in the `parse_url/1` function. The scheme binary extracted from a URL is lowercased and then passed to `binary_to_atom/2`, which unconditionally creates a new BEAM atom. The `#hackney_url{}` record in `include/hackney_lib.hrl` originally typed the `scheme` field as `atom()`, with no allowance for a binary fallback [patch_id=2473695].
What the fix does
The patch replaces `binary_to_atom(SchemeLower, utf8)` with `binary_to_existing_atom(SchemeLower, utf8)` wrapped in a `try/catch` [patch_id=2473695]. When the scheme atom does not already exist, the `badarg` error is caught and the lowercased binary is used instead of minting a new atom [patch_id=2473695]. The `#hackney_url` record spec is widened from `atom()` to `atom() | binary()` to accommodate this change [patch_id=2473695]. Unknown schemes remain as binaries and still reach the existing `{error, {unsupported_scheme, _}}` dispatch branch with the original name intact [patch_id=2473695].
Preconditions
- inputAttacker must be able to supply URLs with attacker-chosen scheme prefixes to hackney
- authNo authentication required; any client or server that feeds URLs to hackney can be leveraged
- networkAttacker can be remote; the attack works over HTTP via redirect chains or direct request targets
Reproduction
Call `hackney_url:parse_url/1` (or `:hackney.request/5`) repeatedly with URLs whose scheme prefixes are unique on each call, e.g. `aaaa://x`, `aaab://x`, `aaac://x`, … After enough iterations, observe `erlang:system_info(:atom_count)` climbing by one per unique scheme [ref_id=1].
Generated on May 25, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
4- github.com/benoitc/hackney/commit/31f6f0e27e096ad88743dfded4f030a3ee74972emitrepatch
- github.com/benoitc/hackney/security/advisories/GHSA-9653-rcfr-5c62mitrevendor-advisoryrelated
- cna.erlef.org/cves/CVE-2026-47067.htmlmitrerelated
- osv.dev/vulnerability/EEF-CVE-2026-47067mitrerelated
News mentions
0No linked articles in our index yet.