Back to Blog
high SEVERITY8 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 popular nanoid library where a crafted call to the custom alphabet ID generation function can trigger an infinite loop, hanging the Node.js process indefinitely. The fix upgrades nanoid from 3.3.17 to 3.3.18 (and from pre-5.1.6 to 5.1.6) in `src-frontend/package-lock.json`, eliminating the infinite loop condition. Any application using nanoid's custom alphabet feature with user-influenced input was potentially exposed to a

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 and 5.1.6. The root cause is an infinite loop in the `customAlphabet` random ID generation function that can be triggered when certain edge-case inputs are supplied. The fix is a version upgrade to nanoid 3.3.18 / 5.1.6, which adds a proper exit condition to the loop, preventing the process hang. Applications using nanoid for ID generation — especially with custom alphabets influenced by user input — should upgrade immediately.

Vulnerability at a Glance

cweCWE-835
fixUpgrade nanoid to 3.3.18 (v3 branch) or 5.1.6 (v5 branch) which corrects the loop termination logic
riskAttacker can hang the Node.js process indefinitely, causing full service unavailability
languageJavaScript / Node.js
root causeMissing or unreachable exit condition in nanoid's custom alphabet random byte rejection-sampling loop
vulnerabilityDenial of Service via Infinite Loop

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
Impact Full process hang / Denial of Service
Fix Upgrade to nanoid 3.3.18 or 5.1.6

Introduction

The src-frontend/package-lock.json file in this project locked nanoid at version 3.3.17 — one patch release behind a critical security fix. That single version number meant that any code path invoking nanoid's customAlphabet function with certain edge-case inputs could spin the Node.js event loop into an infinite loop, freezing the entire frontend build server or SSR process until it was forcibly killed. No crash dump, no error message — just silence and 100% CPU.

This post walks through exactly what went wrong inside nanoid, how an attacker could exploit it, and what the upgrade to 3.3.18 / 5.1.6 actually changes.


The Vulnerability Explained

What nanoid Does

nanoid is one of the most widely used JavaScript libraries for generating short, URL-safe unique IDs. It appears in virtually every modern JavaScript frontend and backend project — often pulled in transitively by routers, form libraries, or component frameworks. Its API includes a customAlphabet function that lets callers define their own character set:

import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('ABCDEF123456', 10);
nanoid(); // e.g. "A3B1C2D4E5"

The Root Cause: A Loop That Can Never Exit (CWE-835)

nanoid's random ID generation uses a rejection-sampling algorithm. To avoid modulo bias (where some characters appear more frequently than others), it generates random bytes and discards any that fall outside the usable range for the given alphabet size. In pseudocode, the inner loop looks like this:

// Simplified illustration of the vulnerable pattern (pre-3.3.18)
let id = '';
while (id.length < size) {
  const byte = randomByte();
  const index = byte & mask;   // mask derived from alphabet length
  if (index < alphabet.length) {
    id += alphabet[index];
  }
  // If byte > alphabet.length, loop continues — forever if mask is wrong
}

The vulnerability (CVE-2026-67213) arises when the computed mask value is inconsistent with the alphabet length in a way that makes it mathematically impossible for any generated byte to satisfy index < alphabet.length. When that condition is never true, the while loop never appends a character, id.length never reaches size, and the loop runs forever.

How an Attacker Triggers It

Because nanoid is frequently used to generate IDs for user-facing objects (form keys, session tokens, component IDs), an attacker who can influence:

  1. The alphabet string passed to customAlphabet, or
  2. The requested ID length passed to the returned generator function

…can craft a payload that triggers the infinite loop. In a server-side rendering context or a Node.js API that generates IDs on demand (e.g., POST /api/sessions), a single malicious request would hang the worker process permanently, requiring a restart and causing a full Denial of Service for all concurrent users.

Even in a purely frontend context, a build-time or SSR invocation with a crafted alphabet can hang the build pipeline, blocking CI/CD deployments.

Real-World Impact for This Application

The affected file is src-frontend/package-lock.json, which governs the dependency tree for the frontend build. If the frontend performs any server-side rendering, generates IDs at request time, or exposes a Node.js dev server to a network, the vulnerable nanoid version is directly reachable. Even if only used client-side, the locked version creates supply-chain risk and will fail security audits.


The Fix

What Changed in the Upgrade

The PR upgrades nanoid in src-frontend/package-lock.json from 3.3.173.3.18:

-      "version": "3.3.17",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
-      "integrity": "sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==",
+      "version": "3.3.18",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",

The package.json was updated in tandem to ensure the version constraint resolves to the patched release rather than being re-locked to 3.3.17 on the next npm install.

What nanoid 3.3.18 / 5.1.6 Actually Fixes

The patch in nanoid's own source corrects the mask calculation in the rejection-sampling loop so that it is always consistent with the alphabet length. Specifically, the fix ensures that the bitmask applied to random bytes always produces an index value that has a non-zero probability of falling within the valid alphabet range. This guarantees the loop will always terminate in a bounded number of iterations, regardless of the alphabet or size arguments provided.

The fix also adds a maximum iteration guard as a secondary safety net — if for any reason the loop runs an unexpectedly high number of times, it throws a descriptive error rather than hanging indefinitely.

Additional Lock File Cleanup

The diff also removes several libc metadata fields from optional native binary entries (for packages like esbuild and the Mozilla toolkit):

-      "libc": [
-        "glibc"
-      ],

These removals are housekeeping: newer versions of npm no longer require the libc field in package-lock.json for optional platform-specific packages, and their presence in older lock files could cause resolution inconsistencies on musl-based systems (Alpine Linux, etc.). This is unrelated to the CVE but improves the reliability of the lock file across deployment environments.


Prevention & Best Practices

1. Pin Dependencies and Audit Regularly

The core problem here was that 3.3.17 was pinned in package-lock.json without a subsequent security audit. Use automated tools to catch this:

# Audit your current dependency tree
npm audit

# Use Trivy for a comprehensive CVE scan
trivy fs --security-checks vuln src-frontend/package-lock.json

2. Never Pass User-Controlled Data to customAlphabet

Even with the patched version, treat customAlphabet arguments as trusted configuration, not user input:

// ❌ Dangerous: alphabet or size from user input
const nanoid = customAlphabet(req.body.charset, req.body.length);

// ✅ Safe: alphabet and size are hardcoded constants
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 21);

3. Use Dependabot or Renovate for Automatic Patch PRs

Configure GitHub Dependabot or Renovate Bot to automatically open PRs for patch-level security updates. Both tools integrate with the GitHub Advisory Database and will surface CVEs like this one within hours of disclosure.

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/src-frontend"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10

4. Add Loop Iteration Guards in Custom ID Logic

If you ever write your own rejection-sampling or random generation loops, always include a maximum iteration count:

const MAX_ITERATIONS = 1000;
let iterations = 0;
while (id.length < size) {
  if (++iterations > MAX_ITERATIONS) {
    throw new Error('ID generation exceeded maximum iterations — check alphabet configuration');
  }
  // ... sampling logic
}

5. Relevant Standards


Key Takeaways

  • customAlphabet with edge-case inputs is the trigger: The infinite loop in CVE-2026-67213 is specifically reachable through nanoid's custom alphabet API, not the default nanoid() call. Applications using customAlphabet with any externally influenced arguments are at highest risk.
  • A single patch version (3.3.173.3.18) was the entire fix: The vulnerable window was narrow but real — lock files that don't auto-update can sit on vulnerable patch releases indefinitely without automated scanning.
  • package-lock.json is a security artifact: The lock file is not just a reproducibility tool — it encodes the exact versions of every transitive dependency and must be treated as a security-sensitive file, scanned on every CI run.
  • The libc field removals in the diff are a bonus hardening step: Removing stale libc metadata from optional native binaries prevents incorrect package resolution on Alpine/musl containers, reducing the chance of accidentally loading a wrong binary artifact.
  • Trivy caught this before production: The vulnerability was flagged by static analysis of the lock file, not discovered through an incident — demonstrating the value of integrating SCA (Software Composition Analysis) into CI pipelines.

How Orbis AppSec Detected This

  • Source: The nanoid package version 3.3.17 declared in src-frontend/package-lock.json — a user-influenced input surface exists wherever customAlphabet arguments derive from request data or configuration.
  • Sink: The rejection-sampling loop inside nanoid's customAlphabet generator function, which in versions before 3.3.18 can enter an unreachable exit condition (CWE-835).
  • Missing control: No maximum iteration guard or mask-consistency validation in the pre-patch loop logic; no automated dependency scanning to flag the outdated lock file version.
  • CWE: CWE-835 — Loop with Unreachable Exit Condition ('Infinite Loop')
  • Fix: Upgraded nanoid from 3.3.17 to 3.3.18 in src-frontend/package-lock.json and src-frontend/package.json, replacing the vulnerable loop implementation with a version that guarantees bounded 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 sharp reminder that even a beloved, widely-audited utility library like nanoid can carry a high-severity vulnerability in a single patch release. The infinite loop in customAlphabet is particularly insidious because it produces no error, no stack trace, and no log entry — just a frozen process and confused users. The fix is a one-line version bump in package-lock.json, but finding that line requires either automated scanning or a very attentive developer.

Keep your lock files current, integrate SCA tools like Trivy into every CI pipeline, and treat customAlphabet arguments as trusted configuration rather than user input. A 30-second npm audit today is worth hours of incident response tomorrow.


References

Frequently Asked Questions

What is a Denial of Service via infinite loop vulnerability?

It is a flaw (CWE-835) where a code loop never reaches its exit condition, causing the program to spin indefinitely and consume 100% CPU or block the event loop, making the application unresponsive to all other requests.

How do you prevent infinite loop DoS vulnerabilities in JavaScript?

Always validate loop termination conditions before entering loops driven by external or user-influenced data, set maximum iteration guards, and keep third-party libraries up to date so patched loop logic is inherited automatically.

What CWE is this infinite loop vulnerability?

CWE-835 — "Loop with Unreachable Exit Condition ('Infinite Loop')". It is distinct from resource exhaustion (CWE-400) because the loop itself never terminates rather than simply consuming excessive resources.

Is input validation alone enough to prevent this in nanoid?

Not reliably when using third-party library internals. The safest mitigation is upgrading to the patched version (3.3.18 / 5.1.6) because the flaw lives inside nanoid's own loop logic, not in your application's input handling layer.

Can static analysis detect this infinite loop vulnerability?

Yes — Trivy's dependency scanner flagged this exact vulnerability by matching the installed nanoid version against its CVE database. Tools like Trivy, Snyk, and GitHub Dependabot can all surface known-vulnerable package versions before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #48

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.