Back to Blog
high SEVERITY5 min read

How Infinite Loop Denial of Service Happens in nanoid and How to Fix It

CVE-2026-67213 is a high-severity infinite loop vulnerability in nanoid's `customAlphabet` function that could cause Denial of Service through CPU exhaustion. The fix upgrades nanoid from 3.3.12 to patched versions 3.3.18 and 5.1.6, eliminating the loop condition that trapped ID generation when processing certain input patterns.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

CVE-2026-67213 is a high-severity infinite loop vulnerability in the nanoid JavaScript library's `customAlphabet` function, classified under CWE-835 (Loop with Unreachable Exit Condition). When nanoid versions before 3.3.18 or 5.1.6 generate random IDs using a custom alphabet, malformed or edge-case input can trigger an infinite loop, causing 100% CPU utilization and complete Denial of Service. The fix requires upgrading nanoid to version 3.3.18 (for v3.x users) or 5.1.6 (for v5.x users), which adds proper bounds checking and loop termination guarantees to the random ID generation algorithm.

Vulnerability at a Glance

cweCWE-835 (Loop with Unreachable Exit Condition)
fixUpgrade nanoid to 3.3.18 or 5.1.6
riskCPU exhaustion causing complete application unresponsiveness
languageJavaScript/Node.js
root causeMissing loop termination condition in customAlphabet random ID generation
vulnerabilityInfinite Loop Denial of Service (CVE-2026-67213)

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:

  1. Bounded iteration counter: The random generation loop now tracks iterations and throws a catchable error after a safety threshold, rather than spinning forever
  2. 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

  1. Automated vulnerability scanning: Integrate Trivy, Snyk, or npm audit into CI pipelines to catch CVEs before deployment
  2. Lockfile integrity: Pin exact versions in package-lock.json and review diff changes in dependency updates
  3. 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 customAlphabet appeared simple but hid a complex edge case in its random sampling
  • The package-lock.json diff 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.


References

Frequently Asked Questions

What is CVE-2026-67213?

CVE-2026-67213 is a high-severity vulnerability in nanoid where the `customAlphabet` function contains an infinite loop that can trigger during random ID generation, causing Denial of Service through CPU exhaustion.

How do you prevent infinite loop DoS in JavaScript libraries?

Always implement explicit loop termination conditions, validate input bounds before entering loops, use timeout guards for potentially unbounded operations, and keep dependencies updated to patch known vulnerabilities.

What CWE is CVE-2026-67213?

CWE-835: Loop with Unreachable Exit Condition ('Infinite Loop')

Is input validation alone enough to prevent this nanoid vulnerability?

No—while input validation helps, the root cause is internal to nanoid's algorithm. The definitive fix is upgrading to patched versions 3.3.18 or 5.1.6 which correct the loop logic itself.

Can static analysis detect infinite loop vulnerabilities?

Yes, static analysis tools like Trivy, Semgrep, and CodeQL can flag potential infinite loops, though some cases require dynamic analysis or fuzzing to identify the specific triggering conditions.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #97

Related Articles

high

How JavaScript Injection via String Interpolation Happens in Go Wails Applications and How to Fix It

A high-severity JavaScript injection vulnerability in `internal/clusterconfigs/input.go` allowed arbitrary code execution through malicious kubeconfig filenames. The `saveClusterConfigFile` function at line 20 constructed JavaScript code by directly interpolating unsanitized filenames into `window.ExecJS()` calls, enabling attackers to break out of string literals and execute arbitrary JavaScript in the Webview context.

high

How Denial of Service via Prototype Pollution happens in Axios and how to fix it

Axios versions prior to 1.15.1 merged untrusted configuration objects without guarding against the `__proto__` key, letting attacker-controlled input pollute `Object.prototype` and crash or destabilize applications. Upgrading axios (and its transitive dependencies `form-data`, `follow-redirects`, `proxy-from-env`) closes this Denial of Service and prototype-pollution attack surface without changing any application code.

critical

How Server-Side Request Forgery happens in Node.js and how to fix it

The order-flow service in a Node.js e-commerce backend built an outbound fetch() URL by directly concatenating a configurable `sendingOrder.url` value with a query string, with no validation of protocol or destination. This allowed order data—including customer and payment-adjacent information—to be silently redirected to an attacker-controlled endpoint simply by changing a config value or environment variable.

critical

How Message Corruption via Protocol Length Header Abuse Happens in WebSocket Implementations and How to Fix It

CVE-2026-54466 is a critical vulnerability in websocket-driver 0.7.4 that allows attackers to corrupt WebSocket messages by abusing protocol length headers. The fix upgrades the package to version 0.7.5, which implements proper validation of untrusted length header inputs. This vulnerability could allow attackers to modify or inject data into real-time communication channels used by frontend applications.

critical

How XML Entity Expansion happens in Node.js and how to fix it

A critical XML External Entity (XXE) vulnerability in `lib/xml2json.js` allowed attackers to trigger exponential memory consumption through nested entity expansion. The fix adds `strictEntities: true` to both SAX parser instances, disabling dangerous entity processing that could crash servers processing untrusted XML.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.