How Denial of Service via Infinite Loop Happens in JavaScript and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-67213 |
| Severity | High |
| Library | nanoid |
| Affected versions | < 3.3.18 (v3), < 5.1.6 (v5) |
| CWE | CWE-835: Loop with Unreachable Exit Condition |
| Impact | Denial of Service (infinite loop, event loop hang) |
| Fix | Upgrade to nanoid 3.3.18 / 5.1.6 |
Summary
CVE-2026-67213 is a high-severity Denial of Service vulnerability in the popular nanoid JavaScript library, where a flaw in the customAlphabet random ID generation function could trigger an infinite loop, hanging the Node.js process indefinitely. The fix upgrades nanoid from version 3.3.11 to 3.3.18 and adds a package.json override to enforce the safe version across the entire dependency tree. Any application using nanoid's custom alphabet feature with attacker-influenced input was potentially at risk of complete availability loss.
Introduction
The client/package-lock.json file in this application locked nanoid at version 3.3.11 — a version containing a subtle but dangerous flaw in its random ID generation engine. Under specific conditions inside the customAlphabet function, nanoid's internal loop could reach a state where its exit condition becomes permanently unreachable, spinning the CPU at 100% and blocking Node.js's single-threaded event loop from processing any other work.
This matters because nanoid is one of the most downloaded JavaScript packages in existence, used by frameworks like Vite, PostCSS, and countless others as a transitive dependency. You may not even be calling nanoid directly — it might be three levels deep in your dependency tree — but a vulnerable version is a vulnerable version, regardless of how it got there.
The Vulnerability Explained
What is nanoid's customAlphabet?
nanoid is a tiny, fast, URL-safe unique string ID generator. Its customAlphabet API lets developers generate IDs using a custom character set:
import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 10);
nanoid(); // => 'a3f2b19c7d'
Under the hood, nanoid uses a rejection-sampling algorithm to ensure uniform randomness. It generates a pool of random bytes, maps each byte to an index in the alphabet, and discards any byte that would introduce statistical bias. The loop continues until enough unbiased characters have been collected to fill the requested ID length.
The Flaw: An Unreachable Loop Exit Condition
The vulnerability (CWE-835) lives in this rejection-sampling loop. In versions before 3.3.18, under certain combinations of alphabet size and requested ID length, the mathematical calculation of the "mask" used to filter random bytes could produce a mask value that causes the loop to reject every single candidate byte — forever. The loop's exit condition (having collected enough valid characters) becomes permanently unreachable.
The result is a tight, synchronous infinite loop that:
- Consumes 100% of one CPU core
- Blocks Node.js's event loop entirely (since JavaScript is single-threaded)
- Prevents the server from responding to any subsequent requests
- Requires a process kill or container restart to recover
The Vulnerable Dependency in package-lock.json
Before the fix, client/package-lock.json pinned nanoid at the vulnerable version:
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
}
And critically, client/package.json had an empty overrides object:
"overrides": {}
An empty override means that any transitive dependency pulling in nanoid — such as Vite, a testing framework, or a CSS toolchain — could resolve to the vulnerable 3.3.11 version, even if the top-level dependency was patched.
Attack Scenario
Consider a web application that uses nanoid (directly or transitively via Vite's dev tooling or a session management library) to generate IDs for user sessions, file uploads, or form tokens. If any code path allows an attacker to influence the alphabet or length parameters passed to customAlphabet — for example, through a query parameter, a configuration endpoint, or even indirectly through a crafted file upload that triggers ID generation — the attacker can send a single HTTP request that causes the server process to spin indefinitely.
Even without direct control over nanoid's parameters, if the vulnerable version is present and a triggerable code path exists, a single malicious request can take down the entire Node.js service.
The Fix
Two Coordinated Changes
The fix required changes to both client/package-lock.json and client/package.json — and understanding why both were necessary is important.
1. package-lock.json: Upgrading the Resolved Version
The lock file now resolves nanoid to the patched version:
"node_modules/nanoid": {
"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=="
}
The integrity hash has changed from the old sha512-N8SpfPUnUp1bK+... to the new sha512-DTg4MJbGMWkfi6VZ..., confirming the package content itself is different — this is not just a metadata change.
2. package.json: Enforcing the Override Across the Dependency Tree
The more critical change is the addition of the overrides field:
Before:
"overrides": {}
After:
"overrides": {
"nanoid": "3.3.18"
}
Without this override, a transitive dependency that declares "nanoid": "^3.3.0" in its own package.json could still resolve to any version in the 3.3.x range — including the vulnerable 3.3.11. The overrides field in npm forces the entire dependency tree to use 3.3.18 for any package that depends on nanoid, regardless of what version range those packages request.
This is the defense-in-depth layer that makes the fix robust. Updating the lock file alone only protects the direct resolution; the override ensures no transitive path can sneak the vulnerable version back in.
Before vs. After at a Glance
| Before | After | |
|---|---|---|
| nanoid version | 3.3.11 |
3.3.18 |
| Integrity hash | sha512-N8SpfPUnUp1bK+... |
sha512-DTg4MJbGMWkfi6VZ... |
| Override enforced | No ({}) |
Yes ("nanoid": "3.3.18") |
| Transitive deps protected | ❌ | ✅ |
Prevention & Best Practices
1. Use npm audit and Dependency Scanners in CI
Integrate npm audit --audit-level=high into your CI pipeline so that high-severity vulnerabilities in dependencies are caught before they reach production:
# In your CI pipeline
- name: Audit dependencies
run: npm audit --audit-level=high
working-directory: client
Tools like Trivy (which detected this CVE), Snyk, and Socket.dev can catch vulnerable dependency versions even before a formal CVE is published.
2. Always Use overrides for Transitive Dependency Vulnerabilities
When a vulnerability exists in a transitive dependency (one you don't directly control), updating the lock file alone is not sufficient. Use npm's overrides (or Yarn's resolutions) to force the entire dependency tree to use the safe version:
// package.json
"overrides": {
"nanoid": "3.3.18"
}
This pattern is essential for supply chain security.
3. Pin Integrity Hashes
The integrity field in package-lock.json is your defense against supply chain attacks. Always commit your lock file and verify that integrity hashes change when you upgrade a package — if a "version bump" doesn't change the hash, something is wrong.
4. Monitor the CWE-835 Pattern in Your Own Code
If you write custom ID generation or token generation logic, be especially careful with rejection-sampling loops. Always verify that the loop's exit condition is mathematically guaranteed to be reachable for all valid inputs:
// Dangerous pattern: loop may never exit if mask calculation is wrong
while (collected.length < targetLength) {
const byte = randomByte();
if (byte & mask) collected.push(alphabet[byte % alphabet.length]);
}
// Safe pattern: add a maximum iteration guard
let attempts = 0;
const MAX_ATTEMPTS = targetLength * 100;
while (collected.length < targetLength && attempts++ < MAX_ATTEMPTS) {
// ...
}
5. Reference Security Standards
- CWE-835: Loop with Unreachable Exit Condition
- OWASP A06:2021: Vulnerable and Outdated Components
- OWASP Dependency Check: Automated dependency vulnerability scanning
Key Takeaways
- nanoid
3.3.11is vulnerable to an infinite loop DoS — if yourpackage-lock.jsonstill references this version, you are exposed even if you never call nanoid directly. - An empty
"overrides": {}inpackage.jsonprovides zero protection — transitive dependencies can still resolve to vulnerable versions; you must explicitly pin the safe version. - The
customAlphabetfunction's rejection-sampling loop is the specific code path where the exit condition becomes unreachable, making this a targeted and deterministic attack vector. - A single HTTP request can hang the entire Node.js event loop — because JavaScript is single-threaded, an infinite synchronous loop in any dependency is a complete availability kill switch.
- Integrity hash verification matters — the hash changed from
sha512-N8SpfPUnUp1bK+...tosha512-DTg4MJbGMWkfi6VZ..., confirming the fix is a real code change, not just a version label.
How Orbis AppSec Detected This
- Source: The vulnerable nanoid
3.3.11package resolved inclient/package-lock.json, potentially reachable via any code path that callscustomAlphabetwith attacker-influenced parameters. - Sink: nanoid's internal rejection-sampling loop inside the
customAlphabetfunction — a synchronous loop that can spin indefinitely when the mask calculation produces an unreachable exit condition. - Missing control: No version constraint or
overridesenforcement inclient/package.jsonto prevent the vulnerable3.3.11version from being resolved transitively. - CWE: CWE-835 — Loop with Unreachable Exit Condition
- Fix: Upgraded nanoid from
3.3.11to3.3.18inpackage-lock.jsonand added"nanoid": "3.3.18"to theoverridesfield inpackage.jsonto enforce the safe version across all transitive dependencies.
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 reminder that even the most innocuous-seeming utility libraries — a 130-byte ID generator — can carry high-severity vulnerabilities that threaten application availability. The infinite loop in nanoid's customAlphabet function is particularly dangerous because it targets Node.js's fundamental single-threaded architecture: one triggered loop means zero responses for every subsequent user.
The fix here is clean and surgical: upgrade to 3.3.18, change the integrity hash, and — critically — add the overrides enforcement so no transitive dependency can drag the vulnerable version back in. Neither change alone is sufficient; both are required for a complete remediation.
For developers building JavaScript applications: treat your package-lock.json as a security artifact, not just a build reproducibility tool. Audit it regularly, enforce overrides for known-vulnerable transitive dependencies, and integrate scanners like Trivy into your CI pipeline so vulnerabilities like this are caught automatically before they reach production.