Back to Blog
high SEVERITY8 min read

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

A high-severity Denial of Service vulnerability (CVE-2026-67213) was discovered in the `nanoid` package used in the `docs-site` component, where a flaw in random ID generation could trigger an infinite loop under certain inputs, exhausting CPU resources. The fix upgrades `nanoid` from version 3.3.15 to 3.3.18 (and 5.x to 5.1.6) in `docs-site/package-lock.json`, closing the attack surface without affecting valid ID generation. This kind of dependency vulnerability is easy to overlook but can have

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

Answer Summary

CVE-2026-67213 is a high-severity Denial of Service vulnerability in the `nanoid` JavaScript package (CWE-835: Loop with Unreachable Exit Condition) where malformed or adversarial input to the random ID generation routine can cause an infinite loop, hanging the Node.js process indefinitely. Affected versions include nanoid 3.x below 3.3.18 and 5.x below 5.1.6. The fix is to upgrade `nanoid` to 3.3.18 or 5.1.6 by updating `docs-site/package-lock.json` and `docs-site/package.json`, which patches the loop termination logic so all inputs resolve correctly.

Vulnerability at a Glance

cweCWE-835
fixUpgrade nanoid to 3.3.18 (v3 branch) or 5.1.6 (v5 branch) in docs-site/package-lock.json
riskAn attacker can hang the server process by triggering the vulnerable nanoid code path, causing complete service unavailability
languageJavaScript / Node.js
root causenanoid's random ID generation loop lacked a guaranteed exit condition under certain edge-case entropy inputs
vulnerabilityDenial of Service via Infinite Loop in Random ID Generation

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


The Vulnerability at a Glance

Field Detail
CVE CVE-2026-67213
Severity HIGH
Package nanoid
Affected versions < 3.3.18 (v3 branch), < 5.1.6 (v5 branch)
Fix Upgrade to nanoid@3.3.18 / nanoid@5.1.6
CWE CWE-835: Loop with Unreachable Exit Condition
File docs-site/package-lock.json

Introduction

The docs-site/package-lock.json file locks the entire dependency tree for a documentation website — including transitive dependencies that most developers never think about. One of those quiet dependencies, nanoid, is a wildly popular unique ID generator used across the JavaScript ecosystem. But in versions below 3.3.18 and 5.1.6, a flaw in its random ID generation routine means that under specific edge-case conditions, the generation loop can spin forever — burning 100% of one CPU core and making the server completely unresponsive.

This is a textbook CWE-835: Loop with Unreachable Exit Condition vulnerability. The fix is straightforward — a dependency upgrade — but understanding why it matters and how it could be exploited is essential for any developer who ships JavaScript to production.


The Vulnerability Explained

What is nanoid?

nanoid is one of the most downloaded npm packages in existence, used to generate cryptographically strong, URL-friendly unique identifiers. A typical call looks like:

import { nanoid } from 'nanoid';
const id = nanoid(); // "V1StGXR8_Z5jdHi6B-myT"

Under the hood, nanoid uses a rejection-sampling algorithm to generate IDs. It draws random bytes from the platform's CSPRNG (e.g., crypto.getRandomValues in browsers, or crypto.randomFillSync in Node.js), maps them to a character alphabet, and discards bytes that fall outside the valid range to avoid modulo bias.

The Flaw: An Infinite Rejection Loop

The vulnerability (CVE-2026-67213) lives in this rejection-sampling loop. In affected versions (nanoid 3.3.15 was pinned in docs-site/package-lock.json), a specific combination of alphabet size and ID length parameters could produce a situation where the probability of accepting any given random byte approaches zero — meaning the loop retries indefinitely and never terminates.

Here is a simplified representation of the vulnerable pattern:

// Simplified vulnerable pattern (nanoid internals, pre-fix)
const generate = (alphabet, size) => {
  let id = '';
  while (id.length < size) {
    const byte = randomByte();
    // If mask is miscalculated, byte is almost always rejected
    if (byte < alphabet.length) {
      id += alphabet[byte];
    }
    // No iteration limit — loop runs forever if byte is always rejected
  }
  return id;
};

The critical missing control is a bounded iteration count or a fallback exit condition. When the mask calculation produces a range that is incompatible with the available entropy, every byte is rejected and id.length never increments toward size.

The Vulnerable Dependency in This Repository

In docs-site/package-lock.json, nanoid was pinned at version 3.3.15:

// Before fix — docs-site/package-lock.json
"nanoid": {
  "version": "3.3.15",
  ...
}

This version predates the loop-termination fix introduced in 3.3.18.

Real-World Attack Scenario

Consider a documentation site that uses a React-based frontend (as this one does — note "react": "^18.2.0" in the diff) with a Node.js build or server-side rendering step. If any server-side code path calls nanoid() with attacker-influenced parameters — for example, a custom alphabet or size derived from a URL query parameter or API input — an attacker could craft a request that drives nanoid into the infinite loop:

GET /api/generate-link?size=999999&alphabet=a HTTP/1.1

If the server passes size and alphabet directly to nanoid(alphabet, size), the process hangs. With enough concurrent requests, the Node.js event loop starves, and the entire service goes down. Even without direct parameter control, the vulnerability can be triggered if any upstream dependency passes edge-case values into nanoid internally.


The Fix

What Changed in the Pull Request

The PR upgrades nanoid from 3.3.15 to 3.3.18 by updating two files:

  • docs-site/package.json
  • docs-site/package-lock.json

The core change in package-lock.json updates the resolved version and integrity hash for the nanoid entry:

// Before
"nanoid": {
  "version": "3.3.15",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
  "integrity": "sha512-<old-hash>"
}

// After
"nanoid": {
  "version": "3.3.18",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
  "integrity": "sha512-<new-hash>"
}

The PR also upgrades several other dependencies as part of the same lockfile refresh:

  • katex: bumped from ^0.18.1 to ^0.18.3
  • tmmcore: bumped from ^0.1.0 to ^0.2.0
  • Multiple platform-specific optional packages had their libc field constraints removed, broadening compatibility

How the Fix Solves the Problem

nanoid 3.3.18 patches the rejection-sampling loop with a bounded retry count and a corrected mask calculation that ensures the acceptance probability remains non-trivially positive for all valid alphabet/size combinations. The fixed internal logic looks conceptually like:

// Fixed pattern (nanoid >= 3.3.18)
const generate = (alphabet, size) => {
  let id = '';
  // Corrected mask ensures bytes are accepted at a reasonable rate
  const mask = (2 << (Math.log(alphabet.length - 1) / Math.LN2)) - 1;
  const step = Math.ceil((1.6 * mask * size) / alphabet.length);

  while (true) {
    const bytes = randomBytes(step);
    for (let i = 0; i < step; i++) {
      const byte = bytes[i] & mask;
      if (byte < alphabet.length) {
        id += alphabet[byte];
        if (id.length === size) return id; // Guaranteed exit
      }
    }
    // step is calculated to make this loop statistically guaranteed to exit
  }
};

The key improvements are:
1. Correct mask calculation — ensures the rejection rate stays low
2. Batch byte generation — processes bytes in statistically sufficient batches
3. Guaranteed termination — the mathematical relationship between step, mask, and alphabet.length ensures the loop exits in finite iterations


Prevention & Best Practices

1. Keep Lock Files Up to Date

A package-lock.json that pins old transitive dependency versions is a liability. Run npm audit and npm update regularly:

# Check for known vulnerabilities
npm audit

# Update all dependencies to latest compatible versions
npm update

# Force-update a specific package
npm install nanoid@latest

2. Integrate Dependency Scanning in CI

This vulnerability was caught by Trivy, a container and filesystem vulnerability scanner. Add it (or a similar tool) to your CI pipeline:

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

Other excellent tools for this:
- npm audit — built into npm, catches known CVEs
- Snyk — deeper dependency graph analysis
- GitHub Dependabot — automatic PRs for vulnerable dependencies
- Socket.dev — behavioral analysis of npm packages

3. Never Pass Unvalidated Input to ID Generators

Even with a patched nanoid, never let user-controlled values dictate the alphabet or size parameters:

// DANGEROUS — never do this
app.get('/id', (req, res) => {
  const id = nanoid(req.query.size); // User controls loop iterations
  res.json({ id });
});

// SAFE — use fixed, validated parameters
const ID_SIZE = 21; // Fixed constant
app.get('/id', (req, res) => {
  const id = nanoid(ID_SIZE);
  res.json({ id });
});

4. Monitor for CWE-835 Patterns in Your Own Code

Any loop in your codebase that depends on probabilistic exit conditions deserves scrutiny. Apply a maximum iteration guard:

// Add iteration limits to any probabilistic loop
const MAX_ATTEMPTS = 1000;
let attempts = 0;
while (condition && attempts < MAX_ATTEMPTS) {
  // ... loop body
  attempts++;
}
if (attempts >= MAX_ATTEMPTS) {
  throw new Error('Max retry attempts exceeded');
}

Relevant Standards


Key Takeaways

  • nanoid 3.3.15 in docs-site/package-lock.json was vulnerable — lock files that pin old transitive dependencies silently carry CVEs forward until explicitly updated.
  • Infinite loop DoS vulnerabilities in ID generators are subtle — the flaw isn't in your application code, it's in a math edge case deep inside a library's rejection-sampling algorithm.
  • katex and tmmcore were also bumped in the same PR — dependency refresh PRs often fix multiple issues simultaneously; don't ignore collateral upgrades.
  • Removing libc constraints from optional packages (seen throughout the diff) broadens platform compatibility and reduces the chance of falling back to vulnerable native binaries on non-glibc systems.
  • Static analysis tools like Trivy can catch CVEs in package-lock.json before they reach production — integrate them early in the development lifecycle, not as an afterthought.

How Orbis AppSec Detected This

  • Source: The nanoid package version 3.3.15 declared in docs-site/package-lock.json — a dependency that flows into any code path invoking nanoid() for ID generation.
  • Sink: The internal rejection-sampling loop inside nanoid/index.js (nanoid v3 branch) — specifically the while loop that generates random bytes and filters them against an alphabet mask, which can spin indefinitely when the mask is miscalculated.
  • Missing control: No maximum iteration bound or statistically guaranteed batch size to ensure the loop terminates within finite steps for all valid alphabet/size combinations.
  • CWE: CWE-835 — Loop with Unreachable Exit Condition (Infinite Loop)
  • Fix: Upgraded nanoid from 3.3.15 to 3.3.18 in docs-site/package-lock.json, which contains the corrected mask calculation and batch-generation logic that guarantees 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 is a reminder that Denial of Service vulnerabilities don't always come from dramatic memory corruption or network floods. Sometimes they come from a math edge case in a 21-character ID generator that's been silently sitting in your lock file for months. The docs-site/package-lock.json file was pinning nanoid@3.3.15 — a version where a specific combination of alphabet size and ID length could cause the random ID generation loop to spin forever, hanging the Node.js process and taking down the service.

The fix is a one-line version bump, but the lesson is broader: your lock file is part of your attack surface. Every pinned version is a commitment to run that exact code in production. Automated scanning tools like Trivy exist precisely to catch these silent regressions before attackers do. Integrate them, act on their findings, and keep your dependency tree current.


References

Frequently Asked Questions

What is a Denial of Service via infinite loop vulnerability?

It is a flaw where a program enters a loop that never terminates because the exit condition can never be reached with certain inputs, consuming 100% CPU and making the application unresponsive.

How do you prevent infinite loop DoS vulnerabilities in JavaScript?

Keep dependencies up to date, pin exact versions in lock files, use tools like Trivy or npm audit in CI, and review loop termination conditions in any code that processes external input.

What CWE is an infinite loop Denial of Service?

CWE-835 — "Loop with Unreachable Exit Condition (Infinite Loop)" covers vulnerabilities where a loop's exit condition can never be satisfied, leading to resource exhaustion.

Is rate limiting enough to prevent this type of DoS?

Rate limiting helps reduce exposure but is not sufficient on its own; the root cause is in the library's internal loop logic, so the only complete fix is patching the vulnerable dependency.

Can static analysis detect infinite loop DoS vulnerabilities in dependencies?

Yes — tools like Trivy, Snyk, and GitHub Dependabot scan dependency trees against known CVE databases and can flag vulnerable versions of packages like nanoid before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #47

Related Articles

high

How Denial of Service via Unbounded Intermediate Arrays happens in JavaScript and how to fix it

CVE-2026-69152 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package (versions prior to 1.1.18/2.1.4/3.0.6/5.0.9) that allows attackers to crash a Node.js application by crafting glob patterns that generate unbounded intermediate arrays, effectively bypassing the earlier CVE-2026-14257 mitigation. The fix upgrades `brace-expansion` from 1.1.14 to 1.1.18 in `frontend/package-lock.json`, closing the bypass and restoring safe memory bounds during pattern expansion.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability (GHSA-5p4m-2wfm-xmqj) was discovered in js-yaml affecting both the 3.x and 4.x branches, where parsing YAML documents containing `!!omap` tags triggers quadratic CPU consumption. The fix upgrades js-yaml from `^4.1.1` to `5.2.0` in the project's GitHub Actions workflow dependencies, closing the attack surface for any untrusted YAML input processed by CI/CD tooling.

critical

How Missing Rate Limiting happens in Express.js and how to fix it

Two public API endpoints in `server.js` — `/api/health` and `/api/contact` — were exposed without any rate limiting middleware, allowing attackers to exhaust server resources or spam an SMTP server with unlimited requests. The fix adds rate limiting to both endpoints, with stricter controls on the resource-intensive `/api/contact` route that triggers email sending operations. This change closes a directly exploitable denial-of-service vector in a production web service.

high

How Denial of Service via Specific Input Sequence happens in JavaScript (marked) and how to fix it

CVE-2026-41680 is a high-severity Denial of Service vulnerability in the marked Markdown parsing library, affecting versions prior to 18.0.2. By supplying a crafted input sequence to the parser, an attacker can cause the application to hang or exhaust resources, making the frontend unavailable. Upgrading marked from 18.0.0 to 18.0.2 in both `package.json` and `package-lock.json` closes the vulnerability without affecting valid Markdown rendering.

high

How Quadratic CPU Consumption happens in JavaScript YAML parsing and how to fix it

A high-severity denial-of-service vulnerability in js-yaml (GHSA-5p4m-2wfm-xmqj) caused quadratic CPU consumption when resolving `!!omap` YAML types in both the 3.x and 4.x branches. The fix upgrades js-yaml from 3.14.2 to 3.15.1 and from 4.1.1 to 4.3.1, eliminating the algorithmic complexity exploit while leaving all valid YAML inputs unaffected.

high

How Denial of Service via Unbounded Data Happens in JavaScript and how to fix it

CVE-2025-58754 is a high-severity Denial of Service vulnerability in the popular axios HTTP client library, caused by the absence of a data size check on incoming response or request payloads. An attacker who can influence the size of data processed by axios could exhaust server memory or CPU, bringing down dependent Node.js applications. The fix upgrades axios from version 1.8.4 to 1.18.0, closing the unbounded data processing path.