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, where crafted brace patterns trigger exponential-time processing that can freeze or crash Node.js applications. The fix upgrades `brace-expansion` from the vulnerable `2.0.3` to the patched `2.1.2` (and aligns related versions to `1.1.16` and `5.0.7`), replacing the nested-scoped vulnerable copy under `node_modules/filelist` and `node_modules/glob` with a single, safe top-level resolution. Any

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

Answer Summary

CVE-2026-13149 is a Denial of Service (DoS) vulnerability (CWE-1333: Inefficient Regular Expression Complexity) in the `brace-expansion` npm package versions prior to 1.1.16, 2.1.2, and 5.0.7. When a specially crafted brace pattern such as `{a,b}{c,d}{e,f}...` (deeply nested or highly combinatorial) is parsed, the algorithm expands combinations in exponential time, causing the Node.js event loop to block. The fix upgrades `brace-expansion` to patched versions by removing the nested vulnerable copy pinned at `2.0.3` under `node_modules/filelist` and `node_modules/glob`, and hoisting a single safe `2.1.2` resolution to the top-level `node_modules/brace-expansion` entry in `package-lock.json`.

Vulnerability at a Glance

cweCWE-1333 (Inefficient Regular Expression Complexity / Algorithmic Complexity)
fixUpgrade brace-expansion to 2.1.2 (and 1.1.16 / 5.0.7 for other resolution slots), removing the nested vulnerable copy from filelist and glob subtrees
riskAn attacker supplying a crafted brace pattern can block the Node.js event loop, causing application unavailability
languageJavaScript / Node.js (npm ecosystem)
root causebrace-expansion 2.0.3 expands combinatorial brace patterns in O(2^n) time with no input length or complexity guard
vulnerabilityDenial of Service via exponential-time brace expansion

The Hidden Bomb in Your Dependency Tree

Not every security vulnerability involves a clever attacker bypassing authentication or injecting SQL. Some of the most impactful attacks are embarrassingly simple: send a single, carefully shaped string and watch a server grind to a halt. CVE-2026-13149 is exactly that kind of vulnerability — lurking inside a transitive npm dependency called brace-expansion, waiting for a string like {a,b}{c,d}{e,f}{g,h}{i,j}{k,l}{m,n}{o,p} to arrive.

This post walks through exactly what went wrong in package-lock.json, why the vulnerable version 2.0.3 of brace-expansion was silently nested inside node_modules/filelist and node_modules/glob, and how the upgrade to 2.1.2 closes the door.


The Vulnerability Explained

What is brace expansion?

Brace expansion is the shell-style feature that turns file{1,2,3}.txt into file1.txt file2.txt file3.txt. The brace-expansion npm package implements this for JavaScript, and it is a transitive dependency of widely-used tools like minimatch and glob — meaning it ends up in almost every Node.js project that does any file-matching.

The exponential-time trap in version 2.0.3

In brace-expansion@2.0.3, the expansion algorithm does not guard against combinatorial explosion. When you provide a pattern with multiple independent brace groups, the number of output strings grows as a product of each group's options. For example:

{a,b,c}{d,e,f}{g,h,i}{j,k,l}{m,n,o}{p,q,r}

This single string expands to 3⁶ = 729 results — still manageable. But add more groups:

{a,b,c,d,e,f,g,h,i,j}{a,b,c,d,e,f,g,h,i,j}{a,b,c,d,e,f,g,h,i,j}...

With enough groups, the expansion count grows past millions and billions. Because Node.js runs JavaScript on a single-threaded event loop, a synchronous computation that takes seconds — or minutes — blocks every other request on the server.

The vulnerable version had no maximum output count, no depth guard, and no timeout. An attacker who can influence a string that eventually reaches brace-expansion's expand() function can trigger this with a payload as short as a few hundred characters.

Where the vulnerable version was hiding

The critical detail in this PR is where 2.0.3 lived. Look at the diff:

-    "node_modules/filelist/node_modules/brace-expansion": {
-      "version": "2.0.3",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
-      "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },

npm's nested node_modules resolution means that filelist (and glob) had their own private copy of brace-expansion@2.0.3 installed at node_modules/filelist/node_modules/brace-expansion. Even if the top-level brace-expansion had been safe, these nested copies would have been loaded by those packages — and they were the vulnerable ones.

Attack scenario

Imagine a build tool or file-watcher that accepts a user-supplied glob pattern via a configuration file or API endpoint, passes it to minimatch or glob, which internally calls brace-expansion. An attacker submits:

{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}

That's 10⁷ = 10 million expansions from a 70-character string. The server's event loop stalls, health checks time out, and the service becomes unavailable — a textbook DoS with zero authentication required.


The Fix

The PR makes two coordinated changes in package-lock.json:

1. Hoist a safe top-level resolution

A new top-level entry for brace-expansion@2.1.2 is added:

+    "node_modules/brace-expansion": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+      "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },

This ensures that any package resolving brace-expansion from the top of the tree gets 2.1.2.

2. Remove the nested vulnerable copies

The nested node_modules/filelist/node_modules/brace-expansion entry pinned at 2.0.3 is deleted entirely, as is the corresponding node_modules/glob/node_modules/brace-expansion block. With those overrides gone, npm's resolution algorithm walks up the tree and finds the safe 2.1.2 at the top level.

Before (vulnerable nested copy):

"node_modules/filelist/node_modules/brace-expansion": {
  "version": "2.0.3",
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
  "integrity": "sha512-MCV/...",
  "dependencies": {
    "balanced-match": "^1.0.0"
  }
}

After (resolved to safe top-level version):

"node_modules/brace-expansion": {
  "version": "2.1.2",
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
  "integrity": "sha512-w5JZc...",
  "license": "MIT",
  "dependencies": {
    "balanced-match": "^1.0.0"
  }
}

The patched 2.1.2 release introduces an expansion guard that limits combinatorial output, preventing the exponential blowup regardless of what pattern is provided.


Prevention & Best Practices

1. Audit your transitive dependency tree regularly

The vulnerable version 2.0.3 was not a direct dependency — it was nested two levels deep under filelist and glob. Tools like npm audit, trivy, and snyk scan the full package-lock.json tree, not just your package.json direct dependencies. Run them in CI on every pull request.

npm audit
# or
trivy fs --scanners vuln .

2. Never trust user-supplied glob/brace patterns without sanitization

If your application accepts glob patterns from users (configuration files, API parameters, CLI arguments), validate them before passing them to minimatch, glob, filelist, or any brace-expansion consumer:

const MAX_PATTERN_LENGTH = 256;

function safeGlob(pattern, options) {
  if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) {
    throw new Error('Invalid or oversized glob pattern');
  }
  return glob(pattern, options);
}

This is defense-in-depth — even on a patched version, limiting input complexity is good hygiene.

3. Lock files are security artifacts

package-lock.json is not just a reproducibility tool — it is a security document. Commit it, review changes to it in PRs, and treat unexpected version bumps in nested node_modules/* entries as potential supply-chain signals.

4. Use overrides in package.json for persistent control

For projects where you cannot immediately update a direct dependency, npm's overrides field forces a safe version across the entire tree:

{
  "overrides": {
    "brace-expansion": ">=2.1.2"
  }
}

5. Relevant standards


Key Takeaways

  • Nested node_modules copies are invisible to casual inspection: brace-expansion@2.0.3 was pinned inside node_modules/filelist/node_modules/ and node_modules/glob/node_modules/, meaning a top-level upgrade alone would not have fixed it — the nested overrides had to be explicitly removed.
  • Exponential complexity DoS requires no authentication: The attack payload for CVE-2026-13149 is a short, valid-looking string — no credentials, no special permissions, no exploit chain.
  • package-lock.json diffs deserve security review: The entire fix lives in package-lock.json, not application code. Teams that skip lock-file review in PRs miss this class of vulnerability entirely.
  • Hoisting a safe version is the right pattern: Adding a top-level node_modules/brace-expansion@2.1.2 entry and removing nested overrides is the canonical npm fix for transitive dependency vulnerabilities — not just patching the direct dependency in package.json.
  • Trivy caught what manual review would likely miss: Static analysis of the full dependency manifest (not just direct deps) is essential for catching deeply nested vulnerable packages like this one.

How Orbis AppSec Detected This

  • Source: User-influenced string input (e.g., glob pattern from configuration or API) passed to glob, minimatch, or filelist
  • Sink: brace-expansion's expand() function called internally by filelist and glob, resolved from the vulnerable nested copy at node_modules/filelist/node_modules/brace-expansion version 2.0.3
  • Missing control: No expansion complexity guard or output-count limit in brace-expansion@2.0.3; no input validation before the pattern reaches the expansion function
  • CWE: CWE-1333 (Inefficient Regular Expression Complexity) / CWE-400 (Uncontrolled Resource Consumption)
  • Fix: Removed the nested brace-expansion@2.0.3 entries under filelist and glob in package-lock.json and added a top-level resolution to the patched brace-expansion@2.1.2

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 are sometimes the quietest ones. brace-expansion is a utility so small and ubiquitous that most developers never think about it — yet a single crafted string routed through it could bring down a Node.js service. The fix here is surgical: hoist a safe version to the top of the dependency tree and remove the nested overrides that were keeping the vulnerable 2.0.3 alive for filelist and glob.

More broadly, this case illustrates why dependency security cannot stop at your direct package.json entries. The real attack surface lives in the full transitive tree locked in package-lock.json, and keeping that tree audited — automatically, on every commit — is the only reliable defense.


References

Frequently Asked Questions

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

It is an attack where an adversary provides input that causes an algorithm to execute in exponential time, exhausting CPU resources and preventing the application from serving other requests.

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

Validate and limit the length/complexity of user-supplied strings before passing them to expansion or regex libraries, and keep dependencies like brace-expansion up to date.

What CWE is algorithmic complexity DoS?

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

Is input length limiting enough to prevent brace-expansion DoS?

It reduces risk significantly, but the most reliable fix is upgrading to a patched version of brace-expansion that guards against combinatorial explosion internally.

Can static analysis detect brace-expansion DoS?

Yes — tools like Trivy and Snyk detect known-vulnerable package versions in package-lock.json, which is exactly how CVE-2026-13149 was identified here.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #42

Related Articles

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 dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

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.

critical

How Remote Code Execution Happens in Handlebars Template Compilation and How to Fix It

CVE-2026-33937 is a critical remote code execution vulnerability in Handlebars.js that allows attackers to execute arbitrary code by passing maliciously crafted Abstract Syntax Tree (AST) objects to the compile() function. The vulnerability was patched in version 4.7.9, and we've upgraded to protect against this threat vector.