How Denial of Service via Infinite Loop Happens in JavaScript and How to Fix It
The Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-67213 |
| Severity | High |
| Package | nanoid |
| Affected versions | < 3.3.18 (v3 branch), < 5.1.6 (v5 branch) |
| Fixed versions | 3.3.18, 5.1.6 |
| CWE | CWE-835: Loop with Unreachable Exit Condition |
| Impact | Denial of Service (process hang) |
Introduction
The bun.lock file in this project pinned nanoid at version 3.3.16 — a seemingly harmless dependency used to generate short, URL-safe unique IDs. But Trivy's dependency scan surfaced CVE-2026-67213: a high-severity flaw hiding inside nanoid's customAlphabet function that can send the JavaScript runtime into an infinite loop, hanging the entire process and denying service to every user.
What makes this particularly insidious is that nanoid is a transitive dependency for many popular packages (including postcss, as shown in the diff). Even if your own code never calls customAlphabet directly, a dependency that does — and that accepts user-influenced alphabet strings — is enough to expose your application.
The Vulnerability Explained
What Is nanoid's customAlphabet?
nanoid is one of the most downloaded npm packages in existence. Its headline feature is generating compact, cryptographically random IDs. The customAlphabet export lets you define your own character set:
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789', 10);
console.log(nanoid()); // e.g., "K3F9QZ1YTW"
Under the hood, nanoid uses a rejection sampling algorithm to avoid modulo bias. It generates random bytes, keeps only those that fall within a usable range for the given alphabet size, and discards the rest. The loop continues until enough valid bytes have been collected to fill the requested ID length.
The Infinite Loop Flaw (CWE-835)
The vulnerability in versions before 3.3.18 / 5.1.6 lies in the loop termination condition inside the customAlphabet implementation. When the alphabet is constructed in a way that causes the rejection mask to discard every generated byte — for example, an alphabet whose length is a power of two minus one, or certain edge-case lengths that interact badly with the bitmask calculation — the loop's exit condition becomes unreachable. The function spins forever, consuming 100% of a CPU core (or blocking a Bun worker thread) without ever returning.
The vulnerable lock file entry looked like this:
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } },
"sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
Attack Scenario
Consider a web application that allows users to customize a short-link alphabet (e.g., "use only these characters in your branded links"). The route handler might look something like:
import { customAlphabet } from 'nanoid';
app.post('/api/shortlink', async (req, res) => {
const { alphabet, length } = req.body;
// Validate length but forget to validate alphabet edge cases
const generate = customAlphabet(alphabet, length);
const id = generate(); // <-- hangs forever with a crafted alphabet
res.json({ id });
});
An attacker sends a single POST request with a specially crafted alphabet value. The generate() call enters the infinite loop. Because Node.js and Bun are single-threaded at the event loop level, this one request blocks all other requests from being processed. The server becomes completely unresponsive. No restart, no timeout, no recovery — until the process is manually killed.
Even without a direct user-facing API surface, a dependency like postcss (which the diff shows also pulling in nanoid@3.3.16 as postcss/nanoid) could be triggered through a crafted CSS input that influences alphabet generation internally.
Real-World Impact
- Full service unavailability — a single malicious request can take down the entire application
- No authentication required — if any public endpoint (directly or indirectly) reaches the vulnerable
customAlphabetpath, the attacker needs no credentials - Transitive exposure — the
postcss/nanoidentry in the lock file confirms this application was exposed throughpostcss, not just direct nanoid usage
The Fix
What Changed in bun.lock
The fix upgrades the top-level nanoid resolution from 3.3.16 to 3.3.18:
Before:
"nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } },
"sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
After:
"nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } },
"sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
Note that the integrity hash changes — this is expected and important. The new hash sha512-DTg4... cryptographically verifies you are running the patched code, not the vulnerable version.
Handling the Transitive postcss/nanoid Dependency
The diff also adds an explicit entry for postcss's pinned nanoid:
"postcss/nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } },
"sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="],
This entry isolates postcss's own pinned version so that the top-level upgrade to 3.3.18 does not silently break postcss's internal expectations. The two entries coexist in the lock file: postcss continues to use its tested version, while the rest of the application benefits from the patched one.
What the Patch Does Internally
In nanoid 3.3.18, the loop termination logic in customAlphabet was corrected so that the rejection mask is computed in a way that always guarantees forward progress. For any alphabet of length ≥ 1, at least some fraction of generated random bytes will be accepted, and the loop will always terminate in a bounded number of iterations. The fix does not change the statistical properties of the generated IDs — outputs remain uniformly distributed and cryptographically random.
Prevention & Best Practices
1. Pin and Audit Your Lock Files
Lock files (bun.lock, package-lock.json, yarn.lock) are your first line of defense. They record exact versions and integrity hashes. Run automated CVE scans against them — Trivy, Snyk, and GitHub Dependabot all support lock file scanning.
# Scan your bun.lock with Trivy
trivy fs --scanners vuln bun.lock
2. Never Pass User-Controlled Data to customAlphabet
If you use nanoid's customAlphabet, treat the alphabet string as a trusted, static value defined at build time — not something derived from user input, environment variables, or database records without strict validation.
// ✅ Safe: alphabet is a hardcoded constant
const nanoid = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 12);
// ❌ Dangerous: alphabet comes from user input
const nanoid = customAlphabet(req.body.alphabet, req.body.length);
3. Validate Alphabet Inputs If Dynamic Use Is Unavoidable
If your use case genuinely requires dynamic alphabets, add strict validation before calling customAlphabet:
function safeCustomAlphabet(alphabet, size) {
if (typeof alphabet !== 'string' || alphabet.length < 2 || alphabet.length > 256) {
throw new Error('Invalid alphabet: must be 2–256 unique characters');
}
if (!Number.isInteger(size) || size < 1 || size > 128) {
throw new Error('Invalid size');
}
return customAlphabet(alphabet, size);
}
4. Keep Dependencies Current with Automated PRs
Tools like Dependabot, Renovate, and Orbis AppSec can open automated upgrade PRs the moment a CVE is published. The faster you merge these, the smaller your exposure window.
5. Relevant Standards
- CWE-835: Loop with Unreachable Exit Condition — https://cwe.mitre.org/data/definitions/835.html
- OWASP A06:2021 – Vulnerable and Outdated Components: Keep all third-party dependencies patched
- OWASP Dependency Check: Automate detection of known-vulnerable libraries in your build pipeline
Key Takeaways
nanoid@3.3.16inbun.lockwas the specific vulnerable artifact — not a hypothetical risk, but a confirmed CVE in the resolved dependency tree.- The
customAlphabetfunction is the dangerous sink — any code path (including transitive dependencies likepostcss) that calls it with non-trivially-sized alphabets is potentially affected. - A single HTTP request is enough — because the infinite loop blocks the event loop, one unauthenticated request can take down the entire Bun/Node.js process.
- Integrity hashes in lock files matter — the new
sha512-DTg4...hash fornanoid@3.3.18ensures you cannot accidentally run the patched version with the old vulnerable binary. - Transitive dependencies require explicit attention — the
postcss/nanoidentry demonstrates that upgrading the top-level package is not always sufficient; lock file entries for nested dependencies must also be audited.
How Orbis AppSec Detected This
- Source: The
bun.lockdependency manifest resolvednanoidto version3.3.16, a version known to contain the vulnerablecustomAlphabetloop logic. - Sink: The
customAlphabetfunction insidenanoid/index.cjs(nanoid@3.3.16) — the loop that generates random IDs using a caller-supplied alphabet string. - Missing control: No upper-bound guard on loop iterations; the rejection-sampling mask could be computed such that no random byte ever passes the acceptance check, making the exit condition permanently false.
- CWE: CWE-835 — Loop with Unreachable Exit Condition
- Fix: Upgraded
nanoidfrom3.3.16to3.3.18inbun.lockandpackage.json, replacing the vulnerable loop termination logic with a version that guarantees forward progress for all valid alphabet inputs.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-67213 is a sharp reminder that even a tiny utility package — one that does nothing more than generate random strings — can carry a high-severity vulnerability capable of taking down your entire service. The infinite loop in nanoid's customAlphabet function required no authentication, no special privileges, and potentially only a single crafted request to exploit. The fix was straightforward: upgrade nanoid to 3.3.18 in bun.lock and package.json. But finding it required automated scanning of the full dependency tree, including transitive dependencies like postcss/nanoid that are easy to overlook in manual reviews.
Keep your lock files scanned, your dependencies current, and your event-loop-blocking code paths defended against untrusted input.