Back to Blog
high SEVERITY7 min read

How Denial of Service via Exponential Regex Complexity Happens in Node.js and How to Fix It

CVE-2026-13149 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package caused by exponential-time complexity when processing certain brace patterns. Because `brace-expansion` is a transitive dependency present in many Node.js projects' production dependency trees, an attacker who can influence glob patterns or file path inputs can trigger runaway CPU consumption and crash the service. The fix upgrades the package to patched versions (1.1.16, 2.1.2, or 5.0.7) and p

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

Answer Summary

CVE-2026-13149 is a Denial of Service vulnerability (CWE-1333, Inefficient Regular Expression Complexity) in the `brace-expansion` npm package affecting Node.js applications. When the library processes specially crafted brace patterns like `{a,b,c,...}` with deeply nested or repeated alternatives, its expansion algorithm exhibits exponential time complexity, consuming all available CPU and hanging the process. The fix is to upgrade `brace-expansion` to version 1.1.16, 2.1.2, or 5.0.7 and explicitly pin it as a direct dependency in `package.json` so the patched version is resolved across all transitive dependents.

Vulnerability at a Glance

cweCWE-1333
fixUpgrade brace-expansion to 1.1.16 / 2.1.2 / 5.0.7 and pin it as an explicit dependency
riskRemote attacker can hang or crash a web service by supplying a crafted brace pattern
languageJavaScript / Node.js
root causebrace-expansion's expansion algorithm has O(2^n) worst-case complexity on certain inputs
vulnerabilityDenial of Service via Exponential-Time Brace Expansion

How Denial of Service via Exponential Regex Complexity Happens in Node.js and How to Fix It

The Vulnerability at a Glance

Field Detail
Vulnerability Denial of Service via Exponential-Time Brace Expansion
CWE CWE-1333 — Inefficient Regular Expression Complexity
Language JavaScript / Node.js
Risk Remote attacker can hang or crash a web service
Root Cause O(2ⁿ) worst-case expansion complexity in brace-expansion
Fix Upgrade to brace-expansion 1.1.16 / 2.1.2 / 5.0.7

Introduction

This high-severity Denial of Service vulnerability could have allowed any remote attacker to monopolize CPU threads in a production web service — with nothing more than a single HTTP request containing a cleverly crafted string. The culprit is brace-expansion, a ubiquitous Node.js utility that expands shell-style brace patterns like {a,b,c} into arrays of strings. It sits deep in the dependency trees of tools like glob, minimatch, and many others, making it nearly invisible — yet its algorithmic flaw, tracked as CVE-2026-13149, is directly reachable in any application that processes user-influenced file paths, glob patterns, or search strings.

The vulnerable package was present in package-lock.json at version 2.1.1. Because this is a web service where request handlers process user-influenced input, Trivy flagged the pattern as likely exploitable in production.


The Vulnerability Explained

What Does brace-expansion Do?

brace-expansion takes a string like file.{js,ts,jsx,tsx} and returns ['file.js', 'file.ts', 'file.jsx', 'file.tsx']. It is used heavily by glob-matching libraries to enumerate possible file paths. The library is downloaded hundreds of millions of times per week on npm.

The Algorithmic Time Bomb

The vulnerability lies in how brace-expansion handles nested or repeated brace groups. Consider a pattern like:

{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}

Each {a,b} doubles the number of combinations. With 10 groups, the library must generate 2¹⁰ = 1,024 strings. With 30 groups, that is over 1 billion strings. The expansion is computed eagerly and synchronously, blocking Node.js's single-threaded event loop entirely.

The vulnerable code path in versions prior to the fix performs a Cartesian-product expansion without any upper bound on output size or time spent. This is a classic CWE-1333 pattern: a function whose execution time grows exponentially with input length.

Pre-Fix State in package-lock.json

Before the fix, package-lock.json resolved brace-expansion to version 2.1.1 through transitive dependencies (notably glob and minimatch). There was no explicit top-level pin, meaning the vulnerable version was silently inherited:

// Before fix — no explicit brace-expansion entry in top-level dependencies
"dependencies": {
  "archiver": "^8.0.0",
  "bcryptjs": "^3.0.3",
  "bullmq": "^5.78.1",
  ...
}

Because brace-expansion@2.1.1 was only a transitive dependency, it was easy to overlook in manual reviews.

Concrete Attack Scenario

This application is a web service. Suppose it exposes an endpoint that accepts a file glob pattern for searching or listing resources — for example:

GET /api/files?pattern={a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}

If the server passes req.query.pattern into any function backed by glob or minimatch (which internally calls brace-expansion), the library will attempt to expand 8⁴ = 4,096 strings synchronously. Scale the exponent up slightly and the event loop is blocked for seconds. Send a handful of concurrent requests and the service is effectively down — no authentication required, no special privileges, just an HTTP GET.

Even without a direct glob endpoint, brace-expansion may be invoked indirectly through build tooling, file-watching middleware, or template engines that process user input.


The Fix

What Changed

The fix makes two targeted changes to package.json and package-lock.json:

1. Explicit top-level dependency pin in package.json:

 "dependencies": {
   "archiver": "^8.0.0",
   "bcryptjs": "^3.0.3",
+  "brace-expansion": "^2.1.2",
   "bullmq": "^5.78.1",
   ...
 }

By adding brace-expansion as a direct dependency, npm is forced to resolve it to ^2.1.2 (the patched version) across the entire dependency tree, overriding any transitive request for 2.1.1.

2. Lock file updated to patched versions:

The package-lock.json now resolves brace-expansion to 2.1.2 (and related packages to their updated equivalents). The lock file diff also updates several @emnapi/* packages that were co-resolved during the upgrade:

-"version": "1.2.1",
-"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
-"integrity": "sha512-uTII7OYF+...",
+"version": "1.2.3",
+"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
+"integrity": "sha512-ELEBe8PsL...",

Why Pinning as a Direct Dependency Matters

npm's dependency resolution algorithm will use the highest satisfying version it finds when multiple packages request the same transitive dependency. However, without an explicit top-level pin, a future npm install could still pull in a vulnerable version if a transitive dependency loosens its own version range. Pinning brace-expansion at the top level guarantees the patched version wins every resolution contest.

What the Patched Versions Fix

Versions 1.1.16, 2.1.2, and 5.0.7 of brace-expansion introduce a complexity guard that limits the number of expansions the algorithm will attempt before throwing an error or returning a safe fallback. This changes the worst-case behavior from O(2ⁿ) unbounded CPU consumption to a fast, bounded failure — exactly the right trade-off for a security fix.


Prevention & Best Practices

1. Audit Transitive Dependencies Regularly

Transitive dependencies are the silent majority of your attack surface. Run npm audit and integrate a dedicated SCA scanner (Trivy, Snyk, Socket.dev) into your CI pipeline so that CVEs in indirect dependencies are caught before they reach production.

# Quick check
npm audit --audit-level=high

# Trivy lock-file scan
trivy fs --scanners vuln package-lock.json

2. Validate and Limit Pattern Inputs

If your application accepts glob patterns or file path expressions from users, impose strict length limits and character allowlists before passing them to any expansion library:

// Example guard before using glob
const MAX_PATTERN_LENGTH = 256;
const SAFE_PATTERN = /^[a-zA-Z0-9_\-\/\.\*\?\[\]{}]+$/;

function safeGlob(pattern) {
  if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) {
    throw new Error('Invalid pattern');
  }
  if (!SAFE_PATTERN.test(pattern)) {
    throw new Error('Pattern contains unsafe characters');
  }
  return glob.sync(pattern);
}

3. Pin Critical Security Dependencies Explicitly

For packages with a history of algorithmic complexity issues (regex engines, parsers, expanders), add them as explicit top-level dependencies in package.json even if you don't use them directly. This gives you control over the resolved version.

4. Use Lockfile Integrity Checks in CI

Commit package-lock.json to version control and use npm ci (not npm install) in CI/CD pipelines. npm ci enforces the exact lock file, preventing silent version drift.

# In CI — always use npm ci
npm ci --ignore-scripts

5. Reference Security Standards


Key Takeaways

  • Transitive dependencies are in scope for attackers. brace-expansion was never imported directly in application code, yet it was reachable through glob/minimatch and exploitable via user-supplied HTTP request parameters.
  • Exponential-time algorithms are DoS vulnerabilities. Any function that performs Cartesian-product expansion, recursive backtracking, or unbounded iteration on user-controlled input is a potential availability risk, not just a performance concern.
  • Pinning brace-expansion explicitly in package.json is the correct fix. Simply upgrading a transitive dependency in the lock file is fragile; adding it as a direct dependency with ^2.1.2 ensures the patched version wins all future resolution conflicts.
  • A single HTTP request is sufficient to exploit this. No authentication, no special permissions — just a crafted pattern string sent to any endpoint that triggers glob matching.
  • Automated scanning of package-lock.json catches what code review misses. Manual review of application source code would never surface this vulnerability; only SCA tooling scanning the lock file found it.

How Orbis AppSec Detected This

  • Source: User-influenced input (e.g., HTTP request query parameters or body fields) passed as glob pattern strings into libraries backed by brace-expansion.
  • Sink: The brace-expansion package's internal expansion function, invoked transitively through glob / minimatch dependencies listed in package-lock.json.
  • Missing control: No upper bound on expansion complexity; no explicit version pin to force resolution to a patched release.
  • CWE: CWE-1333 — Inefficient Regular Expression Complexity (also CWE-400 — Uncontrolled Resource Consumption).
  • Fix: Added "brace-expansion": "^2.1.2" as an explicit top-level dependency in package.json and regenerated package-lock.json to resolve all instances to the patched version.

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-13149 is a reminder that the most dangerous vulnerabilities in modern Node.js applications often hide not in the code you write, but in the packages your packages depend on. A single transitive dependency — brace-expansion at version 2.1.1 — carried an exponential-time complexity flaw that could bring down an entire web service with a single malformed HTTP request. The fix is precise and non-breaking: upgrade to 2.1.2 and pin it explicitly so the resolution is stable across future installs.

Treat your lock file as a security artifact. Scan it, pin critical packages, and automate the detection of CVEs before they reach production.


References

Frequently Asked Questions

What is a Denial of Service via exponential-time complexity?

It is an attack where a crafted input causes an algorithm to run for an exponentially growing amount of time, consuming CPU or memory until the process becomes unresponsive or crashes.

How do you prevent ReDoS and algorithmic complexity DoS in Node.js?

Keep dependencies up to date, pin known-safe versions explicitly, validate and limit the length of any user-supplied glob or pattern strings, and use automated scanners like Trivy or npm audit to catch vulnerable transitive dependencies.

What CWE is exponential-time complexity DoS?

CWE-1333 — Inefficient Regular Expression Complexity (also sometimes categorized under CWE-400, Uncontrolled Resource Consumption).

Is rate-limiting enough to prevent this DoS vulnerability?

Rate-limiting reduces exposure but is not sufficient on its own; a single well-crafted request can still monopolize a CPU thread for seconds or minutes, so patching the library is the only complete fix.

Can static analysis detect this type of vulnerability?

Yes — tools like Trivy, Snyk, and npm audit scan the dependency lock file against known CVE databases and will flag vulnerable versions of brace-expansion before they reach production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #720

Related Articles

high

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.