Back to Blog
high SEVERITY8 min read

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

CVE-2026-67213 is a high-severity Denial of Service vulnerability in the popular nanoid package, where a flaw in the custom alphabet random ID generation path can trigger an infinite loop, hanging the process indefinitely. The fix upgrades nanoid from 3.3.12 to 3.3.18 (and 5.x to 5.1.6), patching the loop condition without changing any public API behavior. Any web application that exposes nanoid's ID generation to user-influenced input should treat this as a priority update.

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

Answer Summary

CVE-2026-67213 is a Denial of Service vulnerability (CWE-835: Loop with Unreachable Exit Condition) in the nanoid JavaScript package. In versions before 3.3.18 / 5.1.6, the `customAlphabet` function contains a loop that can never exit when given certain inputs, causing the Node.js process to hang indefinitely. The fix is a drop-in version upgrade: replace `nanoid@3.3.12` with `nanoid@3.3.18` (or `nanoid@5.1.6` for the v5 line) in your `package.json` and lock file. No API changes are required.

Vulnerability at a Glance

cweCWE-835
fixUpgrade nanoid to 3.3.18 (v3 line) or 5.1.6 (v5 line) where the loop condition is corrected
riskAttacker can hang the server process indefinitely, causing full service unavailability
languageJavaScript / TypeScript (Node.js)
root causeThe `customAlphabet` random ID generator in nanoid contains a loop whose exit condition can never be satisfied for certain inputs
vulnerabilityDenial of Service via Infinite Loop

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


Vulnerability at a Glance

Field Detail
CVE CVE-2026-67213
Severity High
CWE CWE-835 — Loop with Unreachable Exit Condition
Affected package nanoid < 3.3.18 / < 5.1.6
Fix Upgrade to nanoid@3.3.18 or nanoid@5.1.6

Introduction

The bun.lock file in this repository pinned nanoid at version 3.3.12. That version contains a flaw deep inside the customAlphabet random ID generation path: under specific input conditions, the internal rejection-sampling loop never finds a valid byte, spinning the CPU to 100% and blocking the Node.js event loop until the process is killed or the server crashes. Because nanoid is a transitive dependency pulled in by many popular build tools and Vue.js toolchains, this vulnerability is far more widespread than its package name might suggest.

The Trivy scanner detected the pinned "nanoid@3.3.12" entry in bun.lock and matched it against the CVE-2026-67213 advisory. The fix is a two-file change: bump the version in package.json and regenerate bun.lock to record the patched 3.3.18 hash.


The Vulnerability Explained

What nanoid's customAlphabet does

nanoid generates cryptographically random string identifiers. Its customAlphabet(alphabet, size) function lets callers define their own character set. Internally, it uses a rejection-sampling algorithm:

  1. Generate a pool of random bytes.
  2. For each byte, apply a bitmask so the value falls within the alphabet length.
  3. If the masked value is a valid index, accept it; otherwise, reject and retry.

The bitmask is computed as the smallest power-of-two mask that covers the alphabet size. For most alphabet sizes this works fine. However, in vulnerable versions before 3.3.18, a specific combination of alphabet size and pool sizing causes the rejection rate to approach 100%, meaning the inner loop never accumulates enough accepted bytes to fill the requested ID length — it simply loops forever.

The vulnerable entry in bun.lock

# BEFORE (vulnerable)
"nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" },
  "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],

This single line is the evidence Trivy matched: the version string 3.3.12 falls within the vulnerable range < 3.3.18.

How an attacker could exploit this

In this web application context, the attack surface depends on how nanoid is invoked. Consider a common pattern where a server-side route generates a session token or short link using a caller-supplied alphabet:

// Hypothetical vulnerable usage
import { customAlphabet } from 'nanoid'; // 3.3.12

app.post('/shorten', (req, res) => {
  const alphabet = req.body.alphabet ?? 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
  const nanoid = customAlphabet(alphabet, 10);
  res.json({ id: nanoid() }); // ← hangs forever with crafted alphabet
});

An attacker sends a single POST request with a carefully crafted alphabet value that triggers the infinite loop condition. Because Node.js is single-threaded, the event loop is now completely blocked — no other request can be processed. The server is effectively down until restarted. No authentication is required; one HTTP request is sufficient.

Even without direct customAlphabet exposure, the vulnerability can be triggered indirectly through any library that wraps nanoid with a custom alphabet internally.

Real-world impact for this application

This is a Vue 3 web application (evident from the vue@^3.5.13 dependency in bun.lock). The nanoid package is used at minimum as part of the Vite/Vue toolchain. If any server-side route — or a server-side rendering layer — calls nanoid's custom alphabet path with user-influenced parameters, a single malformed request causes a complete DoS. Given the HIGH severity rating, the blast radius justifies an immediate upgrade even before reachability is fully confirmed.


The Fix

What changed in bun.lock

The diff shows a precise, surgical change:

# bun.lock — BEFORE
-    "nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" },
-      "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],

# bun.lock — AFTER
+    "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } },
+      "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],

Three things changed in this one line:

Field Before After Why it matters
Version 3.3.12 3.3.18 Pulls in the patched loop logic
Integrity hash sha512-ZB9RH… sha512-DTg4M… Bun verifies this hash on install; a mismatch would abort the build
bin field "bin/nanoid.cjs" (string) { "nanoid": "bin/nanoid.cjs" } (object) Minor metadata normalisation in the newer release

The package.json change (not shown in the diff snippet) adds "nanoid": "3.3.18" as an explicit direct dependency, which overrides any transitive resolution to the older version:

# package.json — AFTER
+        "nanoid": "3.3.18",

Pinning it explicitly as a direct dependency is the correct approach here: it ensures that even if a transitive dependency still requests ^3.3.0, Bun's resolver honours the explicit override and installs 3.3.18.

Why 3.3.18 fixes the infinite loop

The nanoid maintainers corrected the pool-size calculation in the rejection-sampling loop. The fix ensures the pool is always sized large enough that the probability of filling the requested ID length in a single pass is bounded away from zero — mathematically guaranteeing termination regardless of alphabet size. This is a pure internal logic fix; the public API (nanoid(), customAlphabet(), urlAlphabet) is unchanged.


Key Takeaways

  • nanoid@3.3.12 in bun.lock is directly exploitable — the version string alone is sufficient for Trivy to flag it; no source-code analysis is needed.
  • One crafted HTTP request can block the entire Node.js event loop — because the infinite loop runs synchronously, it starves all other requests, making this a single-packet DoS.
  • Pinning nanoid as a direct dependency in package.json is the right override strategy — it prevents transitive resolution from silently downgrading back to a vulnerable version.
  • The bin field change (stringobject) in bun.lock is a harmless metadata normalisation, not a breaking change — safe to accept as part of the upgrade.
  • Even "build-tool-only" dependencies carry runtime risk in SSR or full-stack Vue applications where the same node_modules tree serves both build and runtime.

How Orbis AppSec Detected This

  • Source: The bun.lock file records nanoid@3.3.12 as a resolved dependency, making the vulnerable version observable to any scanner that reads the lock file.
  • Sink: Any call to customAlphabet() inside nanoid/index.js (v3.3.12) where the rejection-sampling loop iterates without a guaranteed exit — effectively while (id.length < size) { ... } with a miscalculated pool.
  • Missing control: No upper bound or pool-size correction on the rejection-sampling loop; the loop exit condition is mathematically unreachable for certain alphabet sizes.
  • CWE: CWE-835 — Loop with Unreachable Exit Condition (Infinite Loop).
  • Fix: Upgraded nanoid from 3.3.12 to 3.3.18 in both bun.lock and package.json, replacing the vulnerable loop logic with a corrected pool-size calculation.

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 small, widely trusted utility libraries can harbour high-severity vulnerabilities. nanoid is downloaded hundreds of millions of times per month precisely because it is simple and reliable — but version 3.3.12 contains a loop that an attacker can weaponise to take down a Node.js server with a single request. The fix is as simple as a version bump, but the window between disclosure and patching is where real damage happens.

Keep your lock files under continuous scanner scrutiny, treat dependency upgrades as security patches (not just maintenance), and never expose internal ID-generation parameters to user-controlled input. The two-line change shown here — updating the version in package.json and regenerating bun.lock — is all it takes to close this vulnerability entirely.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

Related Articles

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

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

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

How Sensitive Data Exposure happens in Zotero plugins and how to fix it

A high-severity data exposure vulnerability in `Zotero.ts` automatically transmitted complete document metadata—including private notes, attachment paths, and tags—to external LLM services without user consent. The fix replaces broad `item.toJSON()` serialization with explicit field selection, sending only essential bibliographic data.

high

How missing dependency update cooldowns happen in GitHub Dependabot configurations and how to fix it

A semgrep scan flagged `.github/dependabot.yml` for lacking a cooldown period, meaning Dependabot would immediately propose updates to brand-new package versions across npm, Bundler, and Docker ecosystems. The fix adds a `cooldown: default-days: 7` block to every `package-ecosystem` entry, forcing a one-week waiting period before newly published releases are considered — reducing exposure to malicious or unstable package drops.

high

How Path Traversal Happens in TensorFlow's Data Service and How to Fix It

TensorFlow's data service dispatcher validated dataset IDs against forward-slash traversal attacks but overlooked backslash characters on non-Windows platforms, allowing attackers to escape the root directory. A targeted fix adds explicit backslash validation across all platforms, closing a high-severity path traversal vulnerability in the snapshot management system.

critical

How Unbounded WebSocket Message Handling Causes Resource Exhaustion in Node.js and How to Fix It

The WebSocketCrossServerAdapter class in a popular Node.js WebSocket library lacked any rate limiting on inbound messages, allowing attackers to flood Redis nodes and WebSocket servers with high-volume traffic. The fix introduces a configurable `rateLimit` option that caps messages per connection per second, preventing resource exhaustion while preserving legitimate functionality.