Back to Blog
high SEVERITY7 min read

How Infinite Loop Denial of Service happens in JavaScript and how to fix it

A high-severity denial-of-service vulnerability (CVE-2026-67214) was discovered in the popular nanoid package, where passing a specially crafted custom alphabet could trigger an infinite loop, hanging the Node.js process indefinitely. The fix upgrades nanoid from version 3.3.11 to 3.3.17 in `web/package-lock.json` and pins the version via a `package.json` override to prevent transitive dependency drift. Because nanoid is widely used for generating unique IDs across frontend and backend JavaScrip

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-67214 is a high-severity Denial of Service (DoS) vulnerability (CWE-835: Loop with Unreachable Exit Condition) in the JavaScript package nanoid, affecting versions before 3.3.17 (v3 branch) and before 5.1.6 (v5 branch). When a specially crafted custom alphabet is passed to nanoid's `customAlphabet()` function, the internal rejection-sampling loop never terminates, hanging the Node.js process indefinitely. The fix is to upgrade nanoid to 3.3.17 or 5.1.6 and pin the version using a `package.json` overrides field to prevent transitive dependencies from re-introducing the vulnerable version.

Vulnerability at a Glance

cweCWE-835 (Loop with Unreachable Exit Condition)
fixUpgrade nanoid from 3.3.11 to 3.3.17 and pin via package.json overrides to prevent transitive re-introduction
riskAn attacker can hang the Node.js process indefinitely, causing a full application outage
languageJavaScript / Node.js
root causenanoid's rejection-sampling loop in `customAlphabet()` has no exit condition when the alphabet is crafted to make random byte selection always fail
vulnerabilityInfinite Loop Denial of Service via malformed custom alphabet

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

In the web/ frontend of this project, a routine dependency scan flagged a high-severity vulnerability hiding in plain sight inside web/package-lock.json: nanoid version 3.3.11 was pinned as a resolved dependency, and it contained a flaw that could bring a Node.js process to its knees with a single malformed input.

This post breaks down exactly what the vulnerability is, how the infinite loop can be triggered, and what the two-file fix does to close the door permanently.


The Vulnerability Explained

What is nanoid and why does it matter?

nanoid is one of the most downloaded npm packages in existence, used to generate short, URL-safe unique identifiers. It's a staple in frontend frameworks, ORMs, and session management libraries. In this project, it appears as a transitive dependency locked at version 3.3.11 in web/package-lock.json.

The package offers a customAlphabet() function that lets developers define their own character set for generated IDs:

import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('ABCDEFG', 10); // generates 10-char IDs from A-G

This is a powerful feature — but before version 3.3.17, it contained a critical flaw.

The infinite loop in customAlphabet()

Nanoid uses a rejection-sampling algorithm to ensure uniform distribution across the custom alphabet. The algorithm works by:

  1. Generating a batch of random bytes.
  2. Masking each byte against the alphabet length.
  3. Discarding any byte whose masked value falls outside the valid alphabet range.
  4. Repeating until enough valid bytes are collected.

The problem: if the alphabet is crafted so that the mask never produces a value within the valid range, the loop has no reachable exit condition. The process enters a busy-loop, consuming 100% of a CPU core, blocking Node.js's single-threaded event loop, and making the application completely unresponsive.

Vulnerable version locked in the project:

// web/package-lock.json (before fix)
"node_modules/nanoid": {
  "version": "3.3.11",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
  "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
}

Attack scenario

Imagine a web endpoint that accepts a user-supplied alphabet string and uses it to generate a custom ID (for example, a vanity code generator or a configurable slug system):

// Hypothetical vulnerable endpoint
app.post('/generate', (req, res) => {
  const { alphabet, length } = req.body;
  const generate = customAlphabet(alphabet, length); // ← dangerous with 3.3.11
  res.json({ id: generate() });
});

An attacker sends a single POST request with a specially crafted alphabet value. The customAlphabet() call enters the infinite rejection-sampling loop. The Node.js event loop is blocked. Every subsequent request — from every other user — times out. The application is effectively down until the process is restarted.

Even without a direct user-facing customAlphabet() call, a supply-chain compromise or a malicious internal dependency that passes a crafted alphabet could trigger the same outcome.


The Fix

The fix involved two files: web/package-lock.json and web/package.json. Each change serves a distinct purpose.

1. Upgrading the resolved version in package-lock.json

 "node_modules/nanoid": {
-  "version": "3.3.11",
-  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
-  "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+  "version": "3.3.17",
+  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
+  "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",

This bumps the installed version from 3.3.11 to 3.3.17, which contains the patched rejection-sampling loop that correctly handles edge-case alphabets and always terminates.

Notice the integrity hash also changes — this is important. The sha512 integrity field is how npm verifies that the package downloaded from the registry is exactly what was expected. Updating it to match 3.3.17 ensures the supply chain is intact and no tampered package can slip in under the old hash.

2. Pinning via overrides in package.json

+  "overrides": {
+    "nanoid": "3.3.17"
+  }

This is the more subtle — and arguably more important — change. Without this override, any transitive dependency that declares nanoid: "^3.3.0" as its own dependency could cause npm to resolve back to a vulnerable version in a future npm install. The overrides field forces npm to use 3.3.17 for all occurrences of nanoid in the entire dependency tree, regardless of what version other packages request.

Before the fix: A future npm install triggered by adding a new dependency could silently re-introduce nanoid 3.3.11 or another vulnerable 3.x version.

After the fix: The overrides field acts as a permanent guard, ensuring 3.3.17 is always used across the entire web/ dependency graph.


Prevention & Best Practices

1. Run dependency scanners in CI

Tools like Trivy, Snyk, and npm audit maintain up-to-date CVE databases and can flag vulnerable lock file entries before they reach production. This vulnerability was caught by Trivy scanning web/package-lock.json.

# Run Trivy against your project
trivy fs --scanners vuln .

# Or use npm's built-in audit
npm audit --audit-level=high

2. Use overrides (npm) or resolutions (Yarn) defensively

Whenever you patch a transitive dependency, always add a corresponding overrides entry. This prevents regression on the next dependency update cycle.

// package.json
{
  "overrides": {
    "nanoid": "3.3.17"
  }
}

For Yarn workspaces, the equivalent is:

{
  "resolutions": {
    "nanoid": "3.3.17"
  }
}

3. Never pass user-controlled input directly to customAlphabet()

Even with the patched version, passing arbitrary user input as an alphabet is a risky design. Validate and allowlist alphabet characters before use:

const SAFE_ALPHABET_PATTERN = /^[a-zA-Z0-9_-]{2,256}$/;

function safeCustomId(alphabet, length) {
  if (!SAFE_ALPHABET_PATTERN.test(alphabet)) {
    throw new Error('Invalid alphabet');
  }
  return customAlphabet(alphabet, length)();
}

4. Monitor OWASP A06: Vulnerable and Outdated Components

This vulnerability is a textbook example of OWASP Top 10 A06:2021 – Vulnerable and Outdated Components. Establish a regular cadence for reviewing and updating dependencies — monthly at minimum for high-traffic applications.

5. Understand CWE-835

CWE-835: Loop with Unreachable Exit Condition is the root class for this vulnerability. Any loop that relies on external (especially user-controlled) input to reach its termination condition should have a hard iteration cap as a safety net.


Key Takeaways

  • nanoid's customAlphabet() in versions before 3.3.17 (v3) and 5.1.6 (v5) can be triggered into an infinite loop by a crafted alphabet, blocking Node.js's event loop entirely.
  • Updating package-lock.json alone is not enough — without the overrides field in package.json, a future npm install can silently re-introduce the vulnerable version through transitive dependencies.
  • The integrity hash in package-lock.json must match the new version (sha512-xQLf0A3HOMlgHq0n247...) — never copy hashes from old versions when manually editing lock files.
  • A single HTTP request is sufficient to exploit this if customAlphabet() is called with user-controlled input, making this a low-complexity, high-impact attack vector.
  • Trivy caught this in web/package-lock.json without the code path being confirmed reachable — demonstrating the value of scanning lock files, not just runtime code.

How Orbis AppSec Detected This

  • Source: The vulnerable nanoid package version 3.3.11 was resolved and locked in web/package-lock.json as a transitive dependency, reachable from user-facing ID generation logic.
  • Sink: The customAlphabet() function in node_modules/nanoid — specifically its internal rejection-sampling loop — which can spin indefinitely when given a pathological alphabet value.
  • Missing control: No upper bound on the number of rejection-sampling iterations, and no version constraint preventing resolution of the vulnerable 3.3.11 release in the transitive dependency graph.
  • CWE: CWE-835 – Loop with Unreachable Exit Condition
  • Fix: nanoid was upgraded from 3.3.11 to 3.3.17 in web/package-lock.json, and a "overrides": { "nanoid": "3.3.17" } entry was added to web/package.json to prevent transitive re-introduction.

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-67214 is a reminder that even small, widely-trusted utility packages can harbor serious vulnerabilities. A single crafted alphabet value passed to nanoid's customAlphabet() was enough to hang an entire Node.js application. The fix is straightforward — upgrade to 3.3.17 and pin with overrides — but the lesson is broader: dependency hygiene is not a one-time task. Lock files must be scanned continuously, and transitive dependency pins must be enforced at the manifest level, not just in the lock file.

Build security into your dependency workflow the same way you build it into your code.


References

Frequently Asked Questions

What is an infinite loop denial of service vulnerability?

An infinite loop DoS occurs when an attacker can supply input that causes a program's loop to never reach its exit condition, consuming 100% CPU and blocking all other work until the process is killed or crashes.

How do you prevent infinite loop DoS in JavaScript dependencies?

Keep dependencies up to date, use `npm audit` or a scanner like Trivy regularly, and pin sensitive transitive dependencies with the `overrides` field in `package.json` to prevent regressions.

What CWE is infinite loop denial of service?

CWE-835: Loop with Unreachable Exit Condition, which describes loops that can never terminate due to a logic flaw or adversarial input.

Is rate limiting enough to prevent this infinite loop DoS?

No. Rate limiting protects against request-volume attacks, but a single request that triggers the infinite loop will block the event loop entirely, denying service to all other requests regardless of rate limits.

Can static analysis detect this infinite loop vulnerability?

Yes. Tools like Trivy, Snyk, and npm audit maintain CVE databases and can flag known-vulnerable package versions in lock files before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #3

Related Articles

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period, meaning Dependabot would immediately propose updates to newly published packages — including potentially malicious or unstable ones. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` package ecosystem entries, introducing a mandatory 7-day waiting period before any new package version is surfaced as an update candidate.

critical

How CSRF Protection Failures Happen in FastAPI and How to Fix Them

A critical CORS misconfiguration in `backend/main.py` allowed cookies to be sent alongside wildcard-origin requests, violating the CORS specification and opening the door to cross-site request forgery attacks. The fix conditionally disables `allow_credentials` when the allowed origins list contains a wildcard, bringing the configuration into compliance with browser security rules. This change closes a subtle but dangerous gap that could have let attackers on sibling subdomains forge authenticate

critical

How Missing Rate Limiting Happens in Node.js SSE Handlers and How to Fix It

A critical missing rate-limiting control in `src/sse/handlers/chat.js` allowed any caller to flood the SSE chat endpoint with unlimited requests, risking server resource exhaustion, denial of service, and runaway AI provider API costs. The fix introduces a per-IP sliding-window rate limiter that caps requests at 60 per minute and returns HTTP 429 on violations. Because the endpoint was publicly reachable and only validated API keys — not request frequency — exploitation required nothing more tha

medium

How Denial of Service via Catastrophic Backtracking happens in Node.js and how to fix it

CVE-2026-4867 is a Denial of Service vulnerability in path-to-regexp 0.1.12 where malformed URL parameters can trigger catastrophic backtracking in the library's regular expression engine, allowing an attacker to hang or crash a Node.js application with a single crafted request. The fix upgrades path-to-regexp to version 0.1.13, which patches the vulnerable regex patterns. This change was applied via a package-level override to ensure the patched version is used throughout the entire dependency

high

How Denial of Service via Exponential-Time Complexity happens in Node.js and how to fix it

CVE-2026-13149 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package, where crafted input strings trigger exponential-time processing that can freeze or crash a Node.js application. The fix upgrades `brace-expansion` from `2.0.2` to `2.1.4` and `minimatch` from `5.1.6` to `5.1.9`, along with npm `overrides` to ensure the patched versions are used throughout the entire dependency tree.

critical

How Unrestricted File Upload happens in Node.js/Express and how to fix it

A critical unrestricted file upload vulnerability was discovered in `mainsystem/routes/admin/profile.js`, where the avatar upload endpoint accepted any file type without validation. An authenticated attacker could upload a malicious server-side script to a web-accessible directory and execute arbitrary code on the server. The fix adds MIME type filtering, an allowlist of safe image formats, and a 2 MB file size limit to the multer middleware.