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.


Prevention & Best Practices

1. Pin and audit your lock file regularly

Lock files (bun.lock, package-lock.json, yarn.lock) are your first line of defence. Run a vulnerability scanner against them on every CI build:

# With Trivy
trivy fs --scanners vuln bun.lock

# With npm audit (if using npm)
npm audit --audit-level=high

2. Use automated dependency update tools

Tools like Dependabot, Renovate, or Orbis AppSec can open pull requests automatically when a new CVE is published against a pinned version. The faster the patch cycle, the smaller the exposure window.

3. Never pass user-controlled values to customAlphabet

If your application uses nanoid's customAlphabet, treat the alphabet and size parameters as internal constants, not as user inputs:

// ✅ Safe — alphabet is a hard-coded constant
const nanoid = customAlphabet('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', 21);

// ❌ Dangerous — alphabet comes from user input
const nanoid = customAlphabet(req.body.alphabet, req.body.size);

4. Add event-loop monitoring

In production Node.js applications, monitor event-loop lag. A sudden spike to hundreds of milliseconds is a strong signal that an infinite loop or blocking operation has been triggered:

import { monitorEventLoopDelay } from 'perf_hooks';
const h = monitorEventLoopDelay({ resolution: 20 });
h.enable();
setInterval(() => {
  if (h.mean > 100) console.warn(`Event loop lag: ${h.mean}ms`);
}, 5000);

5. Security standards references


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.


References

Frequently Asked Questions

What is a Denial of Service via infinite loop?

It is a class of vulnerability (CWE-835) where an attacker supplies input that causes a program to enter a loop it can never exit, consuming 100% CPU and making the process unresponsive.

How do you prevent infinite loop DoS in JavaScript?

Validate and bound all inputs to loop-dependent logic, use well-maintained library versions, and add timeout guards around any ID generation that accepts external parameters.

What CWE is an infinite loop Denial of Service?

CWE-835 — "Loop with Unreachable Exit Condition (Infinite Loop)."

Is rate-limiting enough to prevent this vulnerability?

Rate-limiting reduces the frequency of triggering the loop but does not prevent a single crafted request from hanging the event loop indefinitely; the root fix is the library upgrade.

Can static analysis detect this vulnerability?

Yes — Trivy's dependency scanner flagged this exact pattern in the bun.lock file by matching the known-vulnerable nanoid version range against the CVE-2026-67213 advisory.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #16

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.