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 Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left this Node.js library without a cooldown period, meaning Dependabot would immediately propose updates to newly published packages — including potentially malicious or unstable ones. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` package ecosystem entries, introducing a mandatory 7-day waiting period before any new package version is surfaced as an update candidate.

critical

How CSRF Protection Failures Happen in FastAPI and How to Fix Them

A critical CORS misconfiguration in `backend/main.py` allowed cookies to be sent alongside wildcard-origin requests, violating the CORS specification and opening the door to cross-site request forgery attacks. The fix conditionally disables `allow_credentials` when the allowed origins list contains a wildcard, bringing the configuration into compliance with browser security rules. This change closes a subtle but dangerous gap that could have let attackers on sibling subdomains forge authenticate

critical

How Missing Rate Limiting Happens in Node.js SSE Handlers and How to Fix It

A critical missing rate-limiting control in `src/sse/handlers/chat.js` allowed any caller to flood the SSE chat endpoint with unlimited requests, risking server resource exhaustion, denial of service, and runaway AI provider API costs. The fix introduces a per-IP sliding-window rate limiter that caps requests at 60 per minute and returns HTTP 429 on violations. Because the endpoint was publicly reachable and only validated API keys — not request frequency — exploitation required nothing more tha

medium

How Denial of Service via Catastrophic Backtracking happens in Node.js and how to fix it

CVE-2026-4867 is a Denial of Service vulnerability in path-to-regexp 0.1.12 where malformed URL parameters can trigger catastrophic backtracking in the library's regular expression engine, allowing an attacker to hang or crash a Node.js application with a single crafted request. The fix upgrades path-to-regexp to version 0.1.13, which patches the vulnerable regex patterns. This change was applied via a package-level override to ensure the patched version is used throughout the entire dependency

high

How Denial of Service via Exponential-Time 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 input strings trigger exponential-time processing that can freeze or crash a Node.js application. The fix upgrades `brace-expansion` from `2.0.2` to `2.1.4` and `minimatch` from `5.1.6` to `5.1.9`, along with npm `overrides` to ensure the patched versions are used throughout the entire dependency tree.

critical

How Unrestricted File Upload happens in Node.js/Express and how to fix it

A critical unrestricted file upload vulnerability was discovered in `mainsystem/routes/admin/profile.js`, where the avatar upload endpoint accepted any file type without validation. An authenticated attacker could upload a malicious server-side script to a web-accessible directory and execute arbitrary code on the server. The fix adds MIME type filtering, an allowlist of safe image formats, and a 2 MB file size limit to the multer middleware.