Back to Blog
high SEVERITY7 min read

How Denial of Service happens in Node.js dependency trees and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-13149) was discovered in the `brace-expansion` package, a transitive dependency used across many Node.js projects. The flaw allows attackers to trigger exponential-time processing by supplying crafted brace patterns, potentially freezing the application. The fix upgrades `brace-expansion` to patched versions (2.1.2, 1.1.16, 5.0.7) and restructures how nested dependencies resolve the package within the `@sentry/bundler-plugin-core` depende

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

Answer Summary

CVE-2026-13149 is a high-severity Denial of Service vulnerability (CWE-1333) in the `brace-expansion` npm package, affecting Node.js applications that process untrusted glob or brace-pattern strings. The vulnerability arises from exponential-time complexity when parsing certain malformed brace expressions, allowing an attacker to freeze the event loop with a small input. The fix upgrades `brace-expansion` to versions 2.1.2, 1.1.16, or 5.0.7 and restructures the `package-lock.json` dependency tree so that `@sentry/bundler-plugin-core/node_modules/minimatch` resolves to the patched version instead of the vulnerable 5.0.6 build.

Vulnerability at a Glance

cweCWE-1333 (Inefficient Regular Expression Complexity)
fixUpgraded `brace-expansion` to 2.1.2 (under `@sentry/bundler-plugin-core/node_modules/minimatch`) and removed the vulnerable 5.0.6 nested resolution
riskAttacker-controlled input can freeze the Node.js event loop, making the application unresponsive
languageJavaScript / Node.js
root causeThe `brace-expansion` package used an algorithm with exponential time complexity when parsing deeply nested or malformed brace patterns
vulnerabilityDenial of Service via exponential-time brace expansion

How Denial of Service Happens in Node.js Dependency Trees and How to Fix It

The package-lock.json file in any non-trivial Node.js project is a sprawling graph of direct and transitive dependencies — and buried inside that graph, a single vulnerable package version can expose your entire application to attack. This post walks through exactly how CVE-2026-13149 works in the brace-expansion package, how it was hiding inside the @sentry/bundler-plugin-core subtree, and what the fix looks like at the lock-file level.


The Vulnerability Explained

What Is brace-expansion and Why Does It Matter?

brace-expansion is a small but widely-used npm package that implements POSIX brace expansion for glob patterns — the same syntax you use when you write src/{components,pages}/**/*.ts in a build tool. It is a dependency of minimatch, which is in turn a dependency of glob, which is used by virtually every build tool, linter, and bundler in the Node.js ecosystem.

Because it sits so deep in the dependency tree, most developers never think about it. That invisibility is exactly what makes CVE-2026-13149 dangerous.

The Root Cause: Exponential-Time Complexity

The vulnerability is classified under CWE-1333 (Inefficient Regular Expression Complexity), but it is not strictly a regex issue — it is an algorithmic complexity problem in how brace patterns are parsed and expanded.

Consider a brace expression like:

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

Expanding this legitimately produces 2¹⁰ = 1,024 combinations. That is fine. But in the vulnerable versions of brace-expansion, certain malformed or deeply nested inputs cause the expansion to grow exponentially in processing time, not just in output size. An attacker can craft an input that takes milliseconds to type but seconds — or minutes — to process.

In a Node.js application, the event loop is single-threaded. If a synchronous call to brace-expansion blocks for even a few seconds, the entire server becomes unresponsive to all other requests. This is a classic Denial of Service via algorithmic complexity, sometimes called a "Billion Laughs"-style attack.

The Vulnerable Code Path in This Repository

The Trivy scanner identified that the project was resolving brace-expansion version 5.0.6 through the following path:

@sentry/bundler-plugin-core
  └── glob@13.0.6
        └── minimatch
              └── brace-expansion@5.0.6   VULNERABLE
                    └── balanced-match@4.0.4

The package-lock.json contained a dedicated nested resolution for this path:

"node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
  "version": "5.0.6",
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
  "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU...",
  "license": "MIT",
  "dependencies": {
    "balanced-match": "^4.0.2"
  },
  "engines": {
    "node": "18 || 20 || >=22"
  }
}

This explicit nested entry meant that even if the top-level brace-expansion was patched, this subtree would continue using version 5.0.6 — the vulnerable one.

Attack Scenario

Imagine a scenario where your Next.js application uses a Server Action that accepts a file glob pattern from the user (e.g., to preview matching files in a project template). Under the hood, that pattern is passed to a glob call, which internally uses minimatch, which uses brace-expansion. An attacker submits a crafted pattern like:

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

The vulnerable brace-expansion@5.0.6 begins expanding this and never finishes in any reasonable time. The Node.js event loop freezes. Every other request to the server — including health checks — times out. The application is effectively down.


The Fix

What Changed in package-lock.json

The fix makes two coordinated changes to the lock file:

Removed — the top-level nested brace-expansion@5.0.6 and its companion balanced-match@4.0.4 under @sentry/bundler-plugin-core:

-    "node_modules/@sentry/bundler-plugin-core/node_modules/balanced-match": {
-      "version": "4.0.4",
-      ...
-    },
-    "node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion": {
-      "version": "5.0.6",
-      ...
-      "dependencies": {
-        "balanced-match": "^4.0.2"
-      },
-    },

Added — a more deeply scoped resolution under minimatch specifically, pinning to the patched brace-expansion@2.1.2 with balanced-match@1.0.2:

+    "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "license": "MIT"
+    },
+    "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/brace-expansion": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
+      ...
+    },

Why This Specific Structure?

The new layout scopes the patched brace-expansion@2.1.2 directly under minimatch's own node_modules directory. This is more precise than the previous approach: instead of overriding brace-expansion for all of @sentry/bundler-plugin-core, it targets exactly the minimatch package that was consuming the vulnerable version. The balanced-match companion is also downgraded from 4.0.4 to 1.0.2, which is the version compatible with the 2.x series of brace-expansion.

The result is a dependency tree where:

@sentry/bundler-plugin-core
  └── glob@13.0.6
        └── minimatch
              ├── node_modules/balanced-match@1.0.2   PATCHED
              └── node_modules/brace-expansion@2.1.2  PATCHED

Version 2.1.2 of brace-expansion includes a fix that bounds the expansion algorithm to polynomial time, eliminating the exponential blowup for malformed inputs.


Prevention & Best Practices

1. Run npm audit in CI

Add npm audit --audit-level=high as a required step in your CI pipeline. This catches known-vulnerable transitive dependencies before they reach production.

npm audit --audit-level=high

2. Use Lock-File Overrides for Deep Transitive Fixes

When a vulnerable package is buried deep in a dependency tree and you cannot wait for upstream maintainers to update, use npm's overrides field in package.json:

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

This forces all resolutions of brace-expansion — regardless of depth — to use a version satisfying the constraint.

3. Scan with Trivy or Snyk Regularly

Trivy (the scanner that caught this issue) can be run locally or in CI:

trivy fs --scanners vuln .

It inspects package-lock.json and flags vulnerable transitive dependencies by CVE ID.

4. Avoid Passing Untrusted Input to Glob Functions

Even with patched libraries, treat glob patterns from user input as untrusted. Validate or sanitize them before passing to glob, minimatch, or any brace-expansion-consuming function:

// Bad: passing raw user input
const files = await glob(req.body.pattern);

// Better: validate against an allowlist of safe characters
const SAFE_GLOB = /^[a-zA-Z0-9/_\-.*{}?,\[\]]+$/;
if (!SAFE_GLOB.test(req.body.pattern)) {
  throw new Error('Invalid glob pattern');
}
const files = await glob(req.body.pattern);

5. Security Standards

  • OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of why dependency hygiene matters.
  • CWE-1333: Inefficient Regular Expression Complexity — applicable to any algorithmic complexity attack, not just regex.

Key Takeaways

  • brace-expansion@5.0.6 inside @sentry/bundler-plugin-core's subtree was the specific vulnerable instance — a reminder that the same package can appear multiple times in a lock file at different versions, and each instance must be audited.
  • Removing the broad @sentry/bundler-plugin-core/node_modules/brace-expansion override and replacing it with a scoped minimatch-level override is a more surgical and maintainable fix than blanket version pinning.
  • Transitive dependencies are attack surface. A package you never import directly can still be the vector for a production outage.
  • balanced-match version matters too — the brace-expansion@2.x series requires balanced-match@1.0.2, not 4.0.4, and getting this pairing wrong would break the fix.
  • Algorithmic complexity attacks require no authentication. Any endpoint that processes user-supplied strings through a vulnerable code path is exposed.

How Orbis AppSec Detected This

  • Source: Untrusted glob or file-pattern strings entering the application (e.g., via HTTP request bodies processed by Next.js Server Actions or build-tool APIs).
  • Sink: The brace-expansion expansion algorithm invoked transitively through minimatchglob@sentry/bundler-plugin-core, as resolved by node_modules/@sentry/bundler-plugin-core/node_modules/brace-expansion@5.0.6.
  • Missing control: No upper bound on expansion complexity; the vulnerable version lacked protection against exponential-time inputs.
  • CWE: CWE-1333 — Inefficient Regular Expression Complexity.
  • Fix: Replaced the brace-expansion@5.0.6 nested resolution under @sentry/bundler-plugin-core with a more precisely scoped brace-expansion@2.1.2 entry under minimatch's own node_modules, paired with the compatible balanced-match@1.0.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 sharp reminder that your application's security posture is only as strong as its deepest transitive dependency. The brace-expansion package is invisible to most developers — it appears nowhere in their own code — yet a single vulnerable version nested inside @sentry/bundler-plugin-core was enough to expose the entire application to a Denial of Service attack.

The fix is precise: remove the broad nested override for brace-expansion@5.0.6, introduce a scoped resolution for brace-expansion@2.1.2 directly under minimatch, and pair it with the correct balanced-match@1.0.2. This surgical approach ensures the vulnerable code path is eliminated without disrupting other parts of the dependency graph.

Make dependency scanning a first-class citizen of your CI pipeline. Run npm audit, integrate Trivy, and use overrides in package.json when you need to force a safe version across the entire tree. The cost of prevention is a few minutes of configuration; the cost of a production DoS is measured in downtime, reputation, and revenue.


References

Frequently Asked Questions

What is a brace-expansion Denial of Service vulnerability?

It is a flaw where specially crafted brace patterns (e.g., `{a,{b,{c,...}}}`) cause the expansion algorithm to run in exponential time, consuming all CPU and blocking the Node.js event loop.

How do you prevent brace-expansion DoS in Node.js?

Keep `brace-expansion` updated to patched versions (≥2.1.2 or ≥1.1.16), audit transitive dependencies regularly with `npm audit`, and avoid passing untrusted strings to glob or minimatch functions.

What CWE is brace-expansion DoS?

CWE-1333 — Inefficient Regular Expression Complexity (also known as ReDoS or algorithmic complexity attacks).

Is input validation alone enough to prevent brace-expansion DoS?

Not reliably. While limiting input length reduces risk, the safest fix is patching the library itself, because the exponential behavior can be triggered with relatively short inputs in vulnerable versions.

Can static analysis detect brace-expansion DoS?

Yes. Tools like Trivy, Snyk, and `npm audit` can flag known-vulnerable versions of `brace-expansion` in your dependency tree, even when the package is a transitive dependency.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1927

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.