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 popular nanoid JavaScript library, where a flaw in the `customAlphabet` random ID generation function could trigger an infinite loop, hanging the Node.js process indefinitely. The fix upgrades nanoid from version 3.3.11 to 3.3.18 (and adds a package-level override to enforce the safe version across the dependency tree) in the client application. Any application using nanoid's custom alphabet feature with attacker-influenced

O
By Orbis AppSec
Published August 22, 2026Reviewed August 22, 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 flaw exists in nanoid's `customAlphabet` random ID generation function, where certain inputs could cause an infinite loop, hanging the Node.js event loop and making the application unresponsive. The fix is to upgrade nanoid to 3.3.18 (v3 branch) or 5.1.6 (v5 branch) and add a `package.json` override to enforce the patched version across all transitive dependencies.

Vulnerability at a Glance

cweCWE-835 (Loop with Unreachable Exit Condition)
fixUpgrade nanoid from 3.3.11 to 3.3.18 and add a package.json override to enforce the patched version across all transitive dependencies
riskAttacker can hang the Node.js event loop, causing complete application unavailability
languageJavaScript / Node.js
root causenanoid's random ID generation logic could enter an infinite loop under certain alphabet/size conditions before version 3.3.18
vulnerabilityDenial of Service via Infinite Loop in nanoid customAlphabet

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


Vulnerability at a Glance

Field Detail
CVE CVE-2026-67213
Severity High
Library nanoid
Affected versions < 3.3.18 (v3), < 5.1.6 (v5)
CWE CWE-835: Loop with Unreachable Exit Condition
Impact Denial of Service (infinite loop, event loop hang)
Fix Upgrade to nanoid 3.3.18 / 5.1.6

Summary

CVE-2026-67213 is a high-severity Denial of Service vulnerability in the popular nanoid JavaScript library, where a flaw in the customAlphabet random ID generation function could trigger an infinite loop, hanging the Node.js process indefinitely. The fix upgrades nanoid from version 3.3.11 to 3.3.18 and adds a package.json override to enforce the safe version across the entire dependency tree. Any application using nanoid's custom alphabet feature with attacker-influenced input was potentially at risk of complete availability loss.


Introduction

The client/package-lock.json file in this application locked nanoid at version 3.3.11 — a version containing a subtle but dangerous flaw in its random ID generation engine. Under specific conditions inside the customAlphabet function, nanoid's internal loop could reach a state where its exit condition becomes permanently unreachable, spinning the CPU at 100% and blocking Node.js's single-threaded event loop from processing any other work.

This matters because nanoid is one of the most downloaded JavaScript packages in existence, used by frameworks like Vite, PostCSS, and countless others as a transitive dependency. You may not even be calling nanoid directly — it might be three levels deep in your dependency tree — but a vulnerable version is a vulnerable version, regardless of how it got there.


The Vulnerability Explained

What is nanoid's customAlphabet?

nanoid is a tiny, fast, URL-safe unique string ID generator. Its customAlphabet API lets developers generate IDs using a custom character set:

import { customAlphabet } from 'nanoid';
const nanoid = customAlphabet('1234567890abcdef', 10);
nanoid(); // => 'a3f2b19c7d'

Under the hood, nanoid uses a rejection-sampling algorithm to ensure uniform randomness. It generates a pool of random bytes, maps each byte to an index in the alphabet, and discards any byte that would introduce statistical bias. The loop continues until enough unbiased characters have been collected to fill the requested ID length.

The Flaw: An Unreachable Loop Exit Condition

The vulnerability (CWE-835) lives in this rejection-sampling loop. In versions before 3.3.18, under certain combinations of alphabet size and requested ID length, the mathematical calculation of the "mask" used to filter random bytes could produce a mask value that causes the loop to reject every single candidate byte — forever. The loop's exit condition (having collected enough valid characters) becomes permanently unreachable.

The result is a tight, synchronous infinite loop that:

  1. Consumes 100% of one CPU core
  2. Blocks Node.js's event loop entirely (since JavaScript is single-threaded)
  3. Prevents the server from responding to any subsequent requests
  4. Requires a process kill or container restart to recover

The Vulnerable Dependency in package-lock.json

Before the fix, client/package-lock.json pinned nanoid at the vulnerable version:

"node_modules/nanoid": {
  "version": "3.3.11",
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
  "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
}

And critically, client/package.json had an empty overrides object:

"overrides": {}

An empty override means that any transitive dependency pulling in nanoid — such as Vite, a testing framework, or a CSS toolchain — could resolve to the vulnerable 3.3.11 version, even if the top-level dependency was patched.

Attack Scenario

Consider a web application that uses nanoid (directly or transitively via Vite's dev tooling or a session management library) to generate IDs for user sessions, file uploads, or form tokens. If any code path allows an attacker to influence the alphabet or length parameters passed to customAlphabet — for example, through a query parameter, a configuration endpoint, or even indirectly through a crafted file upload that triggers ID generation — the attacker can send a single HTTP request that causes the server process to spin indefinitely.

Even without direct control over nanoid's parameters, if the vulnerable version is present and a triggerable code path exists, a single malicious request can take down the entire Node.js service.


The Fix

Two Coordinated Changes

The fix required changes to both client/package-lock.json and client/package.json — and understanding why both were necessary is important.

1. package-lock.json: Upgrading the Resolved Version

The lock file now resolves nanoid to the patched version:

"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 integrity hash has changed from the old sha512-N8SpfPUnUp1bK+... to the new sha512-DTg4MJbGMWkfi6VZ..., confirming the package content itself is different — this is not just a metadata change.

2. package.json: Enforcing the Override Across the Dependency Tree

The more critical change is the addition of the overrides field:

Before:

"overrides": {}

After:

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

Without this override, a transitive dependency that declares "nanoid": "^3.3.0" in its own package.json could still resolve to any version in the 3.3.x range — including the vulnerable 3.3.11. The overrides field in npm forces the entire dependency tree to use 3.3.18 for any package that depends on nanoid, regardless of what version range those packages request.

This is the defense-in-depth layer that makes the fix robust. Updating the lock file alone only protects the direct resolution; the override ensures no transitive path can sneak the vulnerable version back in.

Before vs. After at a Glance

Before After
nanoid version 3.3.11 3.3.18
Integrity hash sha512-N8SpfPUnUp1bK+... sha512-DTg4MJbGMWkfi6VZ...
Override enforced No ({}) Yes ("nanoid": "3.3.18")
Transitive deps protected

Key Takeaways

  • nanoid 3.3.11 is vulnerable to an infinite loop DoS — if your package-lock.json still references this version, you are exposed even if you never call nanoid directly.
  • An empty "overrides": {} in package.json provides zero protection — transitive dependencies can still resolve to vulnerable versions; you must explicitly pin the safe version.
  • The customAlphabet function's rejection-sampling loop is the specific code path where the exit condition becomes unreachable, making this a targeted and deterministic attack vector.
  • A single HTTP request can hang the entire Node.js event loop — because JavaScript is single-threaded, an infinite synchronous loop in any dependency is a complete availability kill switch.
  • Integrity hash verification matters — the hash changed from sha512-N8SpfPUnUp1bK+... to sha512-DTg4MJbGMWkfi6VZ..., confirming the fix is a real code change, not just a version label.

How Orbis AppSec Detected This

  • Source: The vulnerable nanoid 3.3.11 package resolved in client/package-lock.json, potentially reachable via any code path that calls customAlphabet with attacker-influenced parameters.
  • Sink: nanoid's internal rejection-sampling loop inside the customAlphabet function — a synchronous loop that can spin indefinitely when the mask calculation produces an unreachable exit condition.
  • Missing control: No version constraint or overrides enforcement in client/package.json to prevent the vulnerable 3.3.11 version from being resolved transitively.
  • CWE: CWE-835 — Loop with Unreachable Exit Condition
  • Fix: Upgraded nanoid from 3.3.11 to 3.3.18 in package-lock.json and added "nanoid": "3.3.18" to the overrides field in package.json to enforce the safe 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 reminder that even the most innocuous-seeming utility libraries — a 130-byte ID generator — can carry high-severity vulnerabilities that threaten application availability. The infinite loop in nanoid's customAlphabet function is particularly dangerous because it targets Node.js's fundamental single-threaded architecture: one triggered loop means zero responses for every subsequent user.

The fix here is clean and surgical: upgrade to 3.3.18, change the integrity hash, and — critically — add the overrides enforcement so no transitive dependency can drag the vulnerable version back in. Neither change alone is sufficient; both are required for a complete remediation.

For developers building JavaScript applications: treat your package-lock.json as a security artifact, not just a build reproducibility tool. Audit it regularly, enforce overrides for known-vulnerable transitive dependencies, and integrate scanners like Trivy into your CI pipeline so vulnerabilities like this are caught automatically before they reach production.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2113

Related Articles

high

modelExporter.js Path Traversal via Unsanitized Directory Concatenation

A path traversal vulnerability in `modelExporter.js` allowed attackers to read arbitrary files by injecting traversal sequences into directory and relative path parameters. The `readSourceFile` function concatenated these unsanitized inputs directly into file URLs passed to `fetch()`. The fix introduces strict path normalization that rejects attempts to escape the intended directory.

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.

critical

How credential leakage through console logging happens in JavaScript browser extensions and how to fix it

A browser extension's `src/background/credentials.js` printed the full Strava authentication cookie string — including a signed JWT and CloudFront-Signature values — straight into the extension console via `console.debug`. Anyone who could open DevTools on the background page (or any tooling that scraped the console) could copy a live session and impersonate the user. The fix replaces the credential payload in both log statements with `Boolean(credentials)` and strips a realistic-looking JWT out