How Denial of Service via Infinite Loop Happens in Node.js and How to Fix It
The Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-67213 |
| Package | nanoid |
| Affected versions | < 3.3.18 (v3 branch), < 5.1.6 (v5 branch) |
| Severity | HIGH |
| CWE | CWE-835: Loop with Unreachable Exit Condition |
| Fix | Upgrade to nanoid 3.3.18 / 5.1.6 |
Introduction
The package-lock.json file in this project pinned nanoid at version 3.3.16—a version containing a high-severity Denial of Service vulnerability. nanoid is one of the most widely used npm packages for generating compact, URL-safe unique IDs; it appears in the dependency trees of millions of Node.js projects, often pulled in transitively by tools like PostCSS, Vite, or CSS preprocessors rather than as a direct dependency. That ubiquity is exactly what makes CVE-2026-67213 dangerous: you may not even know you're running vulnerable code.
The flaw lives in nanoid's random byte generation loop. Under specific conditions, the loop's exit condition becomes unreachable, causing the Node.js event loop to spin at 100% CPU and never return control to the application. For any server handling user-influenced requests that trigger ID generation, this means a single malicious (or even accidental) request can take down the process entirely.
The Vulnerability Explained
What nanoid Does
nanoid generates short, random, URL-safe strings like V1StGXR8_Z5jdHi6B-myT. It does this by drawing random bytes from a cryptographically secure source and mapping them through an alphabet. The generation loop repeatedly samples random bytes and discards any that fall outside the alphabet's probability range—a standard rejection-sampling pattern.
Where It Goes Wrong
In nanoid 3.3.16, the rejection-sampling loop contained a condition that could, under specific entropy or input conditions, become permanently unsatisfiable. Instead of eventually drawing a valid byte and exiting, the loop would spin indefinitely. This is classified as CWE-835: Loop with Unreachable Exit Condition.
The vulnerable version in package-lock.json before the fix:
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="
}
How an Attacker Exploits This
Consider a web application that uses PostCSS (which depends on nanoid) to process user-uploaded CSS files, or a build API that generates unique asset IDs for uploaded stylesheets. An attacker who can trigger nanoid's ID generation—directly or via a library call—under the conditions that expose the loop bug can cause the Node.js worker process to hang indefinitely.
Concrete attack scenario:
- Attacker sends a POST request to
/api/compile-csswith a crafted stylesheet payload. - The server's PostCSS pipeline calls nanoid internally to generate a unique processing ID.
- nanoid 3.3.16 enters the infinite loop during byte rejection sampling.
- The Node.js event loop is blocked. No other requests are processed.
- The service becomes completely unresponsive until the process is manually restarted.
Because Node.js is single-threaded by default, a single hung request blocks the entire server. Even with a cluster of workers, an attacker can exhaust all workers with a small number of concurrent requests.
Real-World Impact
- Full service outage for any application running a single Node.js process
- Cascading failure in clustered environments if enough workers are targeted simultaneously
- No authentication required if the code path triggering nanoid is publicly accessible
- Hard to diagnose: the process stays "alive" (not crashed), so health checks may not immediately flag the problem
The Fix
What Changed
The fix involves three concrete changes across two files:
1. package-lock.json — Upgraded nanoid from 3.3.16 to 3.3.18
Before:
"node_modules/nanoid": {
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="
}
After:
"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 change confirms a genuinely different package is now installed—not just a metadata update. nanoid 3.3.18 patches the loop condition so the exit path is always reachable, eliminating the infinite spin.
2. package.json — Added an overrides block
"overrides": {
"nanoid": "3.3.18"
}
This is the critical second half of the fix. Without the overrides entry, a transitive dependency (like an older version of PostCSS or another tool) could still resolve its own copy of nanoid at 3.3.16, leaving the vulnerability present in the dependency tree even after package-lock.json is updated. The overrides field in npm 8.3+ forces all packages in the dependency tree to use nanoid 3.3.18, regardless of what version they individually specify.
Why Both Files Matter
| File | Purpose of Change |
|---|---|
package-lock.json |
Records the exact resolved version and integrity hash for the top-level nanoid install |
package.json overrides |
Enforces the patched version across all transitive dependencies that also depend on nanoid |
Updating only package-lock.json would fix the direct dependency but leave transitive copies of nanoid at the vulnerable version. The overrides block closes that gap completely.
Prevention & Best Practices
1. Run Dependency Scanners in CI/CD
Tools like Trivy, npm audit, and Snyk can detect known CVEs in your package-lock.json before they reach production. In this case, Trivy flagged CVE-2026-67213 against the nanoid entry in package-lock.json. Add a step like this to your pipeline:
# Using npm audit
npm audit --audit-level=high
# Using Trivy
trivy fs --exit-code 1 --severity HIGH,CRITICAL .
2. Use overrides for Transitive Dependency Control
When a vulnerability exists in a transitive dependency that you don't control directly, npm's overrides (npm 8.3+) or Yarn's resolutions field lets you enforce a minimum safe version:
// package.json
"overrides": {
"nanoid": ">=3.3.18"
}
3. Pin Lockfiles and Audit Them
Always commit package-lock.json to version control and treat changes to it as security-relevant. A diff showing a version bump in a lockfile should trigger a review against known CVE databases.
4. Monitor Transitive Dependencies
Use npm ls nanoid to see every path in your dependency tree that resolves nanoid:
npm ls nanoid
# my-app@1.0.0
# └─┬ postcss@8.x.x
# └── nanoid@3.3.16 ← vulnerable
This makes transitive exposure visible before scanners flag it.
5. OWASP and CWE References
- OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of why keeping dependencies current matters.
- CWE-835: Loop with Unreachable Exit Condition — the root cause classification for this infinite loop bug.
Key Takeaways
- nanoid 3.3.16's rejection-sampling loop could spin forever, making the Node.js event loop permanently unresponsive on a single malicious or unlucky request.
- Updating
package-lock.jsonalone is not sufficient—transitive copies of nanoid pulled in by tools like PostCSS also needed to be pinned viapackage.json'soverridesblock. - The
overridespattern is essential for enforcing patched versions of transitive dependencies in npm projects; without it, vulnerable sub-dependencies can persist invisibly. - Trivy's static scan of
package-lock.jsoncaught this before any runtime impact—demonstrating that lockfile scanning is a high-value, low-effort security control. - A single hung Node.js process blocks all requests in that worker; DoS via infinite loop is not a theoretical risk but a practical, complete service outage.
How Orbis AppSec Detected This
- Source: The
nanoidpackage version3.3.16recorded inpackage-lock.jsonundernode_modules/nanoid, reachable via the PostCSS dependency chain. - Sink: nanoid's internal random byte generation loop—the function responsible for rejection-sampling random bytes during ID creation—contains the unreachable exit condition.
- Missing control: No version constraint or
overridesentry existed to prevent nanoid3.3.16from being resolved for transitive dependents, leaving the vulnerable loop reachable via any code path that triggers ID generation. - CWE: CWE-835 — Loop with Unreachable Exit Condition.
- Fix: Upgraded nanoid to
3.3.18inpackage-lock.jsonand added"overrides": { "nanoid": "3.3.18" }topackage.jsonto enforce the patched 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 sharp reminder that high-severity vulnerabilities don't always look dramatic in a diff—sometimes the entire fix is a version number change in a lockfile and a four-line overrides block. But the impact of leaving nanoid 3.3.16 in place is anything but subtle: a single request that triggers the infinite loop brings down the Node.js process entirely.
The two-part fix here—bumping the resolved version in package-lock.json and adding an overrides entry in package.json—is the correct, complete remediation pattern for transitive dependency vulnerabilities in npm projects. If your project uses PostCSS, Vite, or any other tool that transitively depends on nanoid, run npm ls nanoid today and make sure you're not still on 3.3.16.