Back to Blog
high SEVERITY9 min read

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

CVE-2026-67213 is a high-severity Denial of Service vulnerability in the nanoid library (versions before 3.3.18 and 5.1.6) where a crafted input to the custom alphabet ID generation function triggers an infinite loop, freezing the Node.js process. The vulnerability was present in the `remotion-composer` package's dependency tree via `package-lock.json`, and was resolved by upgrading nanoid to 3.3.18 and adding a `package.json` override to enforce the patched version across the entire dependency

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

Answer Summary

CVE-2026-67213 is a high-severity Denial of Service (DoS) vulnerability in the nanoid JavaScript library (CWE-835: Loop with Unreachable Exit Condition) affecting versions before 3.3.18 (v3 branch) and 5.1.6 (v5 branch). The flaw exists in nanoid's `customAlphabet` function, where certain inputs cause an infinite loop during random ID generation, hanging the Node.js process indefinitely. The fix upgrades nanoid from 3.3.16 to 3.3.18 in `remotion-composer/package-lock.json` and adds a `"overrides"` field in `package.json` to ensure the patched version is enforced across all transitive dependencies.

Vulnerability at a Glance

cweCWE-835 (Loop with Unreachable Exit Condition)
fixUpgrade nanoid from 3.3.16 to 3.3.18 and add package.json overrides to enforce the patched version
riskAttacker-controlled input to ID generation can freeze the Node.js process indefinitely
languageJavaScript / Node.js
root causenanoid's random byte rejection-sampling loop had no escape condition for certain custom alphabet configurations
vulnerabilityDenial of Service via Infinite Loop in nanoid customAlphabet

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
Library nanoid (< 3.3.18, < 5.1.6)
CWE CWE-835: Loop with Unreachable Exit Condition
Component remotion-composer/package-lock.json
Fix Upgrade to nanoid 3.3.18 + package.json overrides

Introduction

The remotion-composer package uses nanoid as part of its dependency tree — a library trusted by millions of JavaScript projects to generate compact, collision-resistant random IDs. But nanoid versions before 3.3.18 (v3) and 5.1.6 (v5) carry a subtle and dangerous flaw: when customAlphabet is called with certain input configurations, the internal random byte generation loop can spin forever, locking the Node.js event loop and taking the entire application offline.

This is exactly the kind of vulnerability that doesn't announce itself with a crash or an error message. The process simply stops responding — no stack trace, no exception, just silence. For developers relying on nanoid for session tokens, short link generation, or any ID-heavy workload in a server environment, this represents a real availability risk.

Trivy's CVE scanner flagged the vulnerable version (3.3.16) locked in remotion-composer/package-lock.json, triggering this remediation.


The Vulnerability Explained

What nanoid's customAlphabet Does

nanoid's primary appeal is its flexibility. Beyond the default URL-safe alphabet, it exposes a customAlphabet function that lets developers define their own character sets for ID generation:

import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('ABCDEFG', 10);
nanoid(); // → 'BGCAFEDCBA'

Internally, nanoid uses a rejection-sampling algorithm to ensure uniform distribution across the custom alphabet. It generates random bytes, maps them to characters, and discards any bytes that fall outside the valid range — repeating the process until it has collected enough valid characters.

The Infinite Loop Flaw

The vulnerability (CVE-2026-67213) lives in this rejection-sampling loop. In versions before 3.3.18/5.1.6, certain alphabet configurations — particularly very small alphabets or alphabets whose size creates an unfavorable ratio with the random byte pool — can cause the rejection rate to approach 100%. Every generated byte gets discarded, the loop never accumulates enough valid characters, and the function never returns.

Here is the conceptual structure of the vulnerable loop:

// Simplified pseudocode of the vulnerable pattern in nanoid < 3.3.18
function customAlphabet(alphabet, defaultSize = 21) {
  return function nanoid(size = defaultSize) {
    let id = '';
    while (id.length < size) {
      const bytes = random(size); // generate random bytes
      for (let i = bytes.length - 1; i >= 0; i--) {
        // If the byte doesn't map cleanly to the alphabet size,
        // it is discarded — but in edge cases, ALL bytes are discarded
        const byte = bytes[i] & mask;
        if (byte < alphabet.length) {
          id += alphabet[byte];
        }
        // No escape hatch if every byte fails this condition
      }
      // Loop continues forever if no bytes ever pass
    }
    return id;
  };
}

The critical missing control: there is no upper bound on the number of iterations. If the mask and alphabet.length combination results in every sampled byte being rejected, the while (id.length < size) condition is never satisfied, and the function loops indefinitely.

The Vulnerable Dependency in package-lock.json

The remotion-composer/package-lock.json had nanoid pinned at version 3.3.16:

"node_modules/nanoid": {
  "version": "3.3.16",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
  "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="
}

This locked version carries the unpatched loop logic, making any code path that calls customAlphabet with adversarial or edge-case input a potential DoS vector.

Attack Scenario

Consider a Remotion-based rendering service that accepts user-supplied configuration for output file naming, using nanoid with a custom alphabet derived from user input:

// Hypothetical usage pattern in a rendering pipeline
import { customAlphabet } from 'nanoid';

app.post('/render', (req, res) => {
  const { allowedChars } = req.body; // user-controlled!
  const generateId = customAlphabet(allowedChars, 16);
  const jobId = generateId(); // ← HANGS if allowedChars triggers the bug
  startRenderJob(jobId);
  res.json({ jobId });
});

An attacker who can influence the alphabet string — even indirectly through configuration files, API parameters, or environment variables — can send a single request that permanently stalls the Node.js event loop. Because Node.js is single-threaded, one hung request blocks all subsequent requests, achieving a full Denial of Service with minimal effort.

Even without direct user control, an accidental misconfiguration in a build pipeline or CI script could trigger the same outcome.


The Fix

Two-Part Remediation

The fix required changes to both package-lock.json and package.json — and understanding why both were necessary reveals an important lesson about Node.js dependency management.

Part 1: Upgrading the Locked Version in package-lock.json

The direct fix updates the resolved nanoid version 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 new integrity hash (sha512-DTg4...) is cryptographically bound to the patched 3.3.18 tarball, ensuring npm cannot silently substitute a different version.

Part 2: Adding overrides to package.json

Simply updating package-lock.json is not sufficient on its own. Other packages in the dependency tree may themselves depend on nanoid and could resolve to the vulnerable version when npm install is run fresh. The overrides field in package.json solves this:

"overrides": {
  "nanoid": "3.3.18"
}

Before (no overrides):

{
  "devDependencies": {
    "@types/react": "^18.2.0",
    "typescript": "^5.3.0"
  }
}

After (with enforced override):

{
  "devDependencies": {
    "@types/react": "^18.2.0",
    "typescript": "^5.3.0"
  },
  "overrides": {
    "nanoid": "3.3.18"
  }
}

The overrides field (introduced in npm 8.3.0) instructs npm to resolve all occurrences of nanoid — whether direct or transitive — to exactly 3.3.18. This prevents a scenario where a nested dependency like postcss or vite pulls in nanoid 3.3.16 through its own dependency chain.

What Changed in nanoid 3.3.18?

The patch in nanoid 3.3.18 adds a bounded retry mechanism or adjusts the mask calculation to guarantee that the rejection-sampling loop always makes forward progress, regardless of the alphabet configuration. The fix ensures that the loop has a mathematically provable exit condition for all valid alphabet inputs.


Prevention & Best Practices

1. Use overrides (npm) or resolutions (Yarn) for Transitive Vulnerabilities

When a vulnerable package exists deep in your dependency tree, updating only the lock file may not be enough. Always pair lock file updates with an overrides entry:

// package.json (npm)
"overrides": {
  "vulnerable-package": ">=patched-version"
}
// package.json (Yarn)
"resolutions": {
  "vulnerable-package": ">=patched-version"
}

2. Audit Dependencies Regularly

Run automated scanners as part of your CI/CD pipeline:

# npm built-in audit
npm audit

# Trivy for container and filesystem scanning
trivy fs --scanners vuln .

# Snyk
snyk test

3. Never Pass Unvalidated User Input to ID Generation Functions

If your application allows user-configurable alphabets or ID lengths, validate them strictly before passing to nanoid:

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

function safeCustomId(userAlphabet, size = 21) {
  if (!SAFE_ALPHABET_REGEX.test(userAlphabet)) {
    throw new Error('Invalid alphabet configuration');
  }
  return customAlphabet(userAlphabet, size)();
}

4. Pin Exact Versions for Security-Critical Dependencies

For libraries involved in authentication, session management, or ID generation, prefer exact version pinning over range specifiers:

// Prefer this for security-critical deps:
"nanoid": "3.3.18"

// Over this:
"nanoid": "^3.3.0"

5. Monitor CVE Feeds for Your Dependencies

Subscribe to:
- GitHub Security Advisories
- npm Security Advisories
- The nanoid GitHub repository's security tab

Security Standards Reference

  • CWE-835: Loop with Unreachable Exit Condition
  • OWASP A06:2021 – Vulnerable and Outdated Components
  • OWASP DoS Cheat Sheet – guidance on preventing availability attacks

Key Takeaways

  • The customAlphabet function in nanoid < 3.3.18 is the specific vulnerable code path — not the default nanoid() function. If your code uses customAlphabet, this vulnerability is directly relevant to you.
  • Updating package-lock.json alone is insufficient — the "overrides" field in package.json is required to prevent transitive dependencies from re-introducing the vulnerable version during fresh installs.
  • A single hung request can take down an entire Node.js service — because the event loop is single-threaded, one infinite loop blocks all other request handling, making this DoS trivially effective.
  • Trivy detected this at the package-lock.json level — demonstrating that lock file scanning (not just package.json scanning) is essential for catching transitive dependency vulnerabilities.
  • The integrity hash change (sha512-bzlK...sha512-DTg4...) in package-lock.json is a cryptographic guarantee that the patched tarball is being used — always verify integrity hashes when reviewing security upgrades.

How Orbis AppSec Detected This

  • Source: The nanoid package resolved at version 3.3.16 in remotion-composer/package-lock.json, which is consumed by any code path invoking customAlphabet() with user-influenced or edge-case alphabet configurations.
  • Sink: The internal rejection-sampling while loop inside nanoid's customAlphabet implementation — a loop with no upper iteration bound that can spin indefinitely given certain alphabet-to-mask ratios.
  • Missing control: No maximum iteration count or mathematical guarantee that the rejection-sampling loop terminates for all valid alphabet inputs in nanoid versions before 3.3.18.
  • CWE: CWE-835 — Loop with Unreachable Exit Condition
  • Fix: nanoid was upgraded from 3.3.16 to 3.3.18 in package-lock.json, and an "overrides": { "nanoid": "3.3.18" } block was added to package.json to 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 even the most widely trusted, minimal utility libraries can harbor availability-destroying bugs. nanoid is used in hundreds of thousands of JavaScript projects precisely because it's small and fast — but that simplicity masked a loop termination flaw in its customAlphabet implementation that could freeze a Node.js process with a single crafted call.

The remediation for remotion-composer was precise and complete: upgrading the locked version to 3.3.18 and adding an overrides block to prevent the vulnerable version from creeping back through transitive dependencies. Neither change alone would have been sufficient.

For developers building on Node.js, this vulnerability underscores three durable lessons: scan your lock files (not just your manifests), enforce patched versions with overrides, and treat any library involved in ID generation as a security-sensitive component deserving careful version management.


References

Frequently Asked Questions

What is a Denial of Service via infinite loop vulnerability?

It is a flaw where crafted input causes a program to enter a loop that never terminates, consuming 100% CPU and making the application unresponsive to legitimate requests.

How do you prevent infinite loop DoS in JavaScript dependencies?

Pin dependencies to patched versions, use `overrides` in package.json to enforce versions across transitive dependencies, and monitor advisories with tools like Trivy or npm audit.

What CWE is this infinite loop vulnerability?

CWE-835: Loop with Unreachable Exit Condition, which describes loops that can never reach their termination condition given certain inputs.

Is upgrading the direct dependency enough to prevent this vulnerability?

Not always — transitive dependencies may pin an older version. Adding an `"overrides"` block in package.json forces all nested dependency resolutions to use the patched version.

Can static analysis detect this infinite loop vulnerability?

Yes — Trivy's CVE scanner flagged this exact pattern (CVE-2026-67213) in the package-lock.json dependency tree, even before confirming runtime reachability.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #522

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.