Back to Blog
high SEVERITY6 min read

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

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

Answer Summary

CVE-2026-67213 is a Denial of Service vulnerability in the nanoid Node.js package (versions prior to 3.3.18) where crafted input can trigger an infinite loop in random ID generation, starving the event loop. The fix involves upgrading nanoid to 3.3.18 and using npm overrides in package.json to ensure all transitive dependencies resolve to the patched version, preventing exploitation through nested dependency paths.

Vulnerability at a Glance

cweCWE-835 (Loop with Unreachable Exit Condition)
fixUpgrade nanoid to 3.3.18 and add npm overrides to enforce the patched version across all transitive dependencies
riskApplication becomes unresponsive, blocking all requests on the event loop
languageJavaScript (Node.js)
root causenanoid 3.3.17 random ID generation can enter an infinite loop under specific input conditions
vulnerabilityDenial of Service via infinite loop

Introduction

In this project's package-lock.json, the Trivy security scanner flagged a high-severity vulnerability (CVE-2026-67213) in nanoid version 3.3.17 — a widely-used package for generating unique, URL-friendly IDs. The vulnerability allows an attacker to trigger an infinite loop in nanoid's random ID generation logic, effectively causing a Denial of Service by starving the Node.js event loop.

What makes this particularly insidious is that nanoid is rarely a direct dependency developers think about — it's typically pulled in transitively through packages like PostCSS, which itself is used by virtually every modern CSS build pipeline. A vulnerability here can lurk unnoticed deep in the dependency tree while still being exploitable if the application generates IDs based on any user-influenced parameters.

The Vulnerability Explained

How nanoid Works

nanoid generates compact, random string identifiers using a custom alphabet and a cryptographically secure random number generator. The core algorithm repeatedly draws random bytes and maps them to characters in the configured alphabet until it has generated a string of the desired length.

The Infinite Loop Condition

In nanoid 3.3.17, a specific edge case in the random ID generation logic could cause the loop to never terminate. When the internal random byte selection process encounters certain boundary conditions — particularly when the mask calculation for the custom alphabet results in values that consistently fail the character selection criteria — the generation loop spins indefinitely without producing output.

The vulnerable code path looks conceptually like this:

// Simplified vulnerable pattern in nanoid 3.3.17
let id = '';
while (id.length < size) {
  const byte = randomBytes(1)[0] & mask;
  if (byte < alphabet.length) {
    id += alphabet[byte];
  }
  // No exit condition if byte >= alphabet.length consistently
}

If an attacker can influence the alphabet or size parameters (or if specific random byte sequences create a pathological case), the while loop may never complete.

Real-World Impact

In a Node.js application, an infinite loop on the main thread is catastrophic. Because Node.js uses a single-threaded event loop, one stuck operation blocks all incoming requests. An attacker doesn't need to overwhelm the server with traffic — a single carefully crafted request that triggers this code path is enough to take down the entire service.

Attack Scenario

Consider a scenario where the application uses nanoid to generate session tokens, URL slugs, or file identifiers. If any parameter influencing the ID generation (custom alphabet configuration, size, or the random source) can be manipulated through user input or a crafted image/file upload that triggers downstream ID generation, an attacker could:

  1. Send a request that triggers nanoid's ID generation with pathological parameters
  2. The event loop enters an infinite loop
  3. All subsequent HTTP requests queue up and time out
  4. The application becomes completely unresponsive

The Fix

The fix involves two coordinated changes across package.json and package-lock.json:

Change 1: Upgrade the resolved version in package-lock.json

// Before (vulnerable)
"node_modules/nanoid": {
  "version": "3.3.17",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
  "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g=="
}

// After (patched)
"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=="
}

This ensures the directly resolved version is patched.

Change 2: Add npm overrides in package.json

// Added to package.json
"overrides": {
  "nanoid": "3.3.18"
}

This is the critical piece. The overrides field in package.json forces all instances of nanoid in the entire dependency tree to resolve to version 3.3.18, regardless of what version ranges transitive dependencies (like PostCSS) specify. Without this override, running npm install could still resolve nested dependencies to the vulnerable 3.3.17 version.

Why Both Changes Are Necessary

  • package-lock.json change: Updates the currently locked version so that the exact installed version is 3.3.18.
  • package.json override: Ensures future npm install operations don't regress by allowing transitive dependencies to pull in older versions. This is a defense-in-depth measure that survives lock file regeneration.

The patched version (3.3.18) adds proper bounds checking and loop termination guarantees to the ID generation algorithm, ensuring that even under pathological random byte sequences, the function will always complete in bounded time.

Prevention & Best Practices

1. Use Dependency Scanning in CI/CD

Integrate tools like Trivy, Snyk, or npm audit into your CI pipeline to catch vulnerable dependencies before they reach production:

# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'

2. Leverage npm Overrides for Transitive Dependencies

When a vulnerability exists in a transitive dependency that you don't directly control, npm overrides (or yarn resolutions) let you enforce a minimum version:

{
  "overrides": {
    "nanoid": ">=3.3.18"
  }
}

3. Implement Request Timeouts

Even with patched dependencies, defense-in-depth means adding server-level timeouts:

const server = app.listen(3000);
server.timeout = 30000; // 30 second timeout
server.requestTimeout = 30000;

4. Consider Worker Threads for CPU-Bound Operations

For operations that could potentially block the event loop, use worker threads:

const { Worker } = require('worker_threads');
// Offload potentially blocking operations

5. Regular Dependency Audits

Run npm audit regularly and configure Dependabot or Renovate for automated dependency updates.

Key Takeaways

  • Transitive dependencies are attack surface: nanoid wasn't a direct dependency but was still exploitable through the dependency tree. The npm overrides field is essential for enforcing patched versions in nested dependencies.
  • A single infinite loop kills a Node.js server: Unlike multi-threaded runtimes, one blocked operation on Node.js's event loop denies service to all concurrent users — no traffic amplification needed.
  • Lock files alone don't prevent regression: Updating package-lock.json fixes the current install, but without overrides in package.json, a future npm install or lock file regeneration could reintroduce the vulnerable version.
  • ID generation libraries need bounded execution guarantees: Any loop that generates output character-by-character must have a maximum iteration count or alternative exit condition to prevent infinite execution.
  • Scanner tools like Trivy catch what manual review misses: The vulnerability was in a dependency four levels deep — automated scanning was essential for detection.

How Orbis AppSec Detected This

  • Source: The nanoid package (version 3.3.17) present in package-lock.json as a transitive dependency, reachable through PostCSS and other CSS tooling dependencies
  • Sink: nanoid's internal ID generation loop function, which can be triggered by any code path that calls nanoid() or customAlphabet() for generating random identifiers
  • Missing control: No bounds on loop iterations in the random byte selection algorithm; no npm override to enforce patched version across transitive dependencies
  • CWE: CWE-835 (Loop with Unreachable Exit Condition)
  • Fix: Upgraded nanoid from 3.3.17 to 3.3.18 in package-lock.json and added an npm override in package.json to enforce the patched version across all transitive dependency paths

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 demonstrates a recurring pattern in the Node.js ecosystem: high-severity vulnerabilities hiding in transitive dependencies that developers never directly interact with. The nanoid infinite loop vulnerability could take down an entire application with a single malicious request, yet it existed several layers deep in the dependency tree.

The fix — upgrading to nanoid 3.3.18 with an npm override — is minimal in code changes but maximal in security impact. It's a reminder that dependency management is an active security practice, not a one-time setup. Keep your scanners running, your overrides current, and your timeouts configured.

References

Frequently Asked Questions

What is a Denial of Service via infinite loop?

It's a vulnerability where specific input causes a program to enter a loop that never terminates, consuming CPU resources indefinitely and making the application unresponsive to legitimate requests.

How do you prevent infinite loop DoS in Node.js?

Keep dependencies updated, use npm overrides to enforce patched versions across transitive dependencies, implement request timeouts, and use worker threads for CPU-intensive operations to prevent event loop blocking.

What CWE is infinite loop DoS?

CWE-835 (Loop with Unreachable Exit Condition) — a weakness where a loop cannot reach its exit condition, consuming excessive resources.

Is upgrading just the direct dependency enough to prevent this?

No — transitive dependencies may still resolve to the vulnerable version. Using npm overrides (as done in this fix) ensures all instances of nanoid in the dependency tree are upgraded.

Can static analysis detect infinite loop vulnerabilities in dependencies?

Yes — tools like Trivy, Snyk, and npm audit scan dependency manifests (package-lock.json) against vulnerability databases and can flag known CVEs in both direct and transitive dependencies.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1187

Related Articles

high

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

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

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

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

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

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.

high

How Shell Injection via os.system() happens in Python and how to fix it

A shell injection vulnerability in TensorFlow's DELF dataset download script allowed attackers who controlled the `data_dir` parameter to execute arbitrary shell commands by injecting metacharacters into `os.system()` calls. The fix replaces all four `os.system()` invocations with `subprocess.run()` using argument lists, eliminating shell interpretation entirely. This change closes a high-severity code execution path in production ML infrastructure.