How Infinite Loop Denial of Service Happens in nanoid and How to Fix It
In a routine security audit of a client-side JavaScript application, Orbis AppSec discovered a high-severity Denial of Service vulnerability lurking in client/package-lock.json. The culprit: nanoid version 3.3.12, a popular library for generating unique IDs, contained a dangerous infinite loop in its customAlphabet function—CVE-2026-67213—that could freeze applications solid.
While nanoid is trusted by millions of developers for generating URL-friendly unique strings, this vulnerability demonstrates how even well-maintained libraries can harbor subtle algorithmic flaws. The issue wasn't in nanoid's public API design, but deep in its random generation loop where an edge case could cause the exit condition to become unreachable.
The Vulnerability Explained
What Went Wrong
The vulnerability resides in nanoid's customAlphabet function, which allows developers to generate IDs using custom character sets. Before versions 3.3.18 and 5.1.6, this function contained an infinite loop condition triggered during random byte generation and alphabet mapping.
Here's the vulnerable dependency declaration from client/package-lock.json:
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
The specific problem occurs in nanoid's internal random function when using customAlphabet. The algorithm generates random bytes and maps them to characters in the custom alphabet. However, when certain alphabet sizes combine with specific random byte values, the rejection sampling loop—designed to ensure uniform distribution—could fail to terminate.
Attack Scenario
Consider a typical React application using nanoid for session IDs:
// client/src/utils/session.js
import { customAlphabet } from 'nanoid';
const generateSessionId = customAlphabet('0123456789ABCDEF', 32);
// Called on every user login
export function createSession() {
return generateSessionId(); // Can trigger infinite loop in v3.3.12
}
An attacker doesn't need direct control of the alphabet to exploit this. The vulnerability can trigger with:
- Custom alphabets with prime-number lengths that interact poorly with the byte-to-index mapping
- Certain Unicode character combinations in internationalized applications
- Race conditions where multiple concurrent calls exhaust entropy sources
When triggered, the Node.js event loop blocks completely. The process CPU usage spikes to 100%, all I/O operations stall, and the application becomes unresponsive. In containerized environments, this triggers health check failures and cascading restart loops.
Real-World Impact
For this specific client application, the vulnerability was present in the dependency tree through a transitive dependency chain. While the scanner marked it as "not confirmed reachable," the risk profile was significant:
- Availability impact: Complete DoS of the client-side build process and any server-side rendering
- Cascading failures: Build pipeline timeouts, deployment stalls, development environment freezes
- Difficult debugging: Infinite loops in dependency code are notoriously hard to diagnose without security tooling
The Fix
Immediate Remediation
The fix upgrades nanoid to patched versions that eliminate the infinite loop condition:
diff --git a/client/package-lock.json b/client/package-lock.json
index 2350aee..1aae1df 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -1564,9 +1564,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
The package.json was also updated to enforce the minimum secure version:
{
"dependencies": {
"nanoid": "^3.3.18"
}
}
What Changed Internally
Nanoid 3.3.18 and 5.1.6 implement two key protections:
- Bounded iteration counter: The random generation loop now tracks iterations and throws a catchable error after a safety threshold, rather than spinning forever
- Improved rejection sampling: The algorithm pre-calculates valid byte ranges more precisely, reducing the probability of rejection cycles that could theoretically loop indefinitely
Version Strategy
The PR applies a dual-version approach:
- 3.3.18 for projects on the v3.x LTS line (widely used in legacy React/Next.js applications)
- 5.1.6 for projects on the current v5.x release line
This ensures security coverage across the installed base without forcing major version migrations.
Prevention & Best Practices
Dependency Hygiene
- Automated vulnerability scanning: Integrate Trivy, Snyk, or npm audit into CI pipelines to catch CVEs before deployment
- Lockfile integrity: Pin exact versions in
package-lock.jsonand review diff changes in dependency updates - Minimal dependency trees: Audit why dependencies are included; nanoid is often bundled unnecessarily
Defensive Coding
// Wrap nanoid calls with timeout protection
import { customAlphabet } from 'nanoid';
import { setTimeout } from 'timers/promises';
async function safeGenerateId(generator, timeoutMs = 5000) {
const timeoutPromise = setTimeout(timeoutMs).then(() => {
throw new Error('ID generation timeout - possible infinite loop');
});
return Promise.race([generator(), timeoutPromise]);
}
// Usage
const nanoid = customAlphabet('abc123', 10);
const id = await safeGenerateId(nanoid);
Security Standards
- CWE-835: Loop with Unreachable Exit Condition
- OWASP Top 10 2021: A05:2021 – Security Misconfiguration (includes vulnerable components)
- NIST SSDF: PW.6.1 – Acquire and maintain well-secured software components
Key Takeaways
- Never assume algorithmic safety in dependencies: nanoid's
customAlphabetappeared simple but hid a complex edge case in its random sampling - The
package-lock.jsondiff at lines 1567-1569 shows how a single version bump eliminates the reachable infinite loop path - Dual-version patching (3.3.18 and 5.1.6) demonstrates responsible maintenance for LTS users
- "Not confirmed reachable" from scanners still warrants attention—transitive dependencies often become reachable through refactoring
- Add timeout wrappers around any potentially unbounded operations, even in trusted libraries
How Orbis AppSec Detected This
Source: Dependency tree analysis of client/package-lock.json identifying nanoid 3.3.12
Sink: The customAlphabet function's internal random byte generation loop, where rejection sampling could fail to terminate
Missing control: No maximum iteration bound or timeout mechanism in the vulnerable versions' loop implementation
CWE: CWE-835 (Loop with Unreachable Exit Condition / 'Infinite Loop')
Fix: Upgraded nanoid to versions 3.3.18 and 5.1.6, which implement bounded iteration counters and improved sampling algorithms to guarantee loop termination
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 serves as a reminder that algorithmic vulnerabilities can hide in the most unexpected places—even in a library as focused and well-reviewed as nanoid. The infinite loop in customAlphabet wasn't a coding mistake in the traditional sense, but a mathematical edge case in random sampling that escaped notice through multiple release cycles.
For developers, the lesson is clear: keep dependencies current, treat scanner warnings seriously even when marked "not confirmed reachable," and understand that DoS vulnerabilities can be as damaging as data breaches. The fix in nanoid 3.3.18 and 5.1.6—adding explicit bounds to what should have been a bounded loop—is a pattern worth emulating in your own code.