Back to Blog
high SEVERITY8 min read

How Denial of Service happens in Node.js brace-expansion and how to fix it

A high-severity denial-of-service vulnerability (CVE-2026-14257) was discovered in the `brace-expansion` npm package through version 5.0.7, affecting projects that transitively depend on it via tools like `@sentry/bundler-plugin-core` and `@typescript-eslint/typescript-estree`. The fix removes pinned vulnerable copies of `brace-expansion@2.1.2` nested inside `minimatch` sub-dependencies and allows the dependency tree to resolve to patched versions (5.0.8, 3.0.3, 2.1.3, or 1.1.17). Left unpatched

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

Answer Summary

CVE-2026-14257 is a high-severity denial-of-service vulnerability in the `brace-expansion` npm package (all versions through 5.0.7) caused by uncontrolled regex expansion when parsing malicious brace patterns, mapped to CWE-400 (Uncontrolled Resource Consumption). In this repository, vulnerable copies of `brace-expansion@2.1.2` were pinned as nested dependencies inside `@sentry/bundler-plugin-core` and `@typescript-eslint/typescript-estree` via their `minimatch` sub-dependencies. The fix removes those pinned nested entries from `package-lock.json` so npm resolves them to patched versions (2.1.3, 3.0.3, 5.0.8, or 1.1.17), eliminating the DoS vector without changing any application behavior.

Vulnerability at a Glance

cweCWE-400
fixRemove pinned vulnerable brace-expansion@2.1.2 nested entries; resolve to patched versions 2.1.3 / 3.0.3 / 5.0.8 / 1.1.17
riskAttacker-controlled input can exhaust CPU, making the application unresponsive
languageJavaScript / Node.js
root causebrace-expansion ≤5.0.7 does not bound the number of expansions generated from a crafted brace pattern
vulnerabilityDenial of Service via uncontrolled brace-pattern expansion

Introduction

The package-lock.json file in a Node.js project is often treated as a boring implementation detail — a machine-generated lockfile that nobody reads. But it is precisely because nobody reads it that dangerous vulnerability patterns can hide inside it for months. In this repository, the Trivy scanner surfaced two separate pinned copies of brace-expansion@2.1.2 buried deep in the nested dependency trees of @sentry/bundler-plugin-core and @typescript-eslint/typescript-estree. Both copies were vulnerable to CVE-2026-14257, a high-severity denial-of-service flaw that allows an attacker to exhaust server CPU by feeding a malicious brace pattern to the library.

This post walks through exactly where those copies lived, why they were dangerous, how the fix removes them, and what you can do to prevent similar issues in your own projects.


The Vulnerability Explained

What is brace-expansion?

brace-expansion is a tiny but widely-used npm package that implements POSIX-style brace expansion — the same feature your shell uses when you type cp file.{js,ts,json}. It is a direct dependency of minimatch, which is in turn used by virtually every glob-matching library in the Node.js ecosystem.

The CVE-2026-14257 Flaw

In brace-expansion versions through 5.0.7, the expansion algorithm does not place any upper bound on the number of strings it will generate from a single input pattern. Consider a pattern like:

{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{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 output. Twenty of them produce 2²⁰ = 1,048,576 strings. Thirty produce over a billion. The library will happily attempt to allocate and return all of them, saturating the event loop and exhausting heap memory.

This is a classic CWE-400: Uncontrolled Resource Consumption pattern — the library trusts that its caller will only pass it reasonable input, but makes no defensive check itself.

Where the Vulnerable Code Lived in This Repository

The tricky part here is that the top-level project might not directly depend on brace-expansion. The vulnerability existed in two nested locations inside package-lock.json:

Location 1 — @sentry/bundler-plugin-core's private minimatch copy:

"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",
  "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
  "dependencies": {
    "balanced-match": "^1.0.0"
  }
}

Location 2 — @typescript-eslint/typescript-estree's private minimatch copy:

"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
  "version": "2.1.2",
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
  "integrity": "sha512-w5JZcKgdhDOgOwm8H+K..."
}

Both entries pin brace-expansion at exactly 2.1.2 — a version that predates the security fix. Because package-lock.json is authoritative, npm ci will always install this exact version regardless of what the parent package's package.json says.

Attack Scenario

Imagine a CI/CD pipeline or a developer tooling server that:

  1. Accepts a user-supplied file glob pattern (e.g., through a build configuration API or a lint-on-save editor plugin).
  2. Passes that pattern to minimatch or a glob library that internally calls brace-expansion.

An attacker who can influence that pattern — even indirectly, through a crafted .eslintrc file in a pull request or a malicious Sentry source-map configuration — could submit:

{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}

This single pattern expands to 10⁸ = 100 million strings. The Node.js process will attempt to build that array, spike to 100% CPU, and become unresponsive. In a shared CI environment, this could block all pipelines.


The Fix

What Changed

The fix is surgical: it removes the two pinned nested brace-expansion@2.1.2 entries (and their associated balanced-match copies) from package-lock.json. With those entries gone, npm's dependency resolution algorithm is free to satisfy the brace-expansion requirement using a patched version from higher up in the tree.

Before (vulnerable — two blocks removed):

-    "node_modules/@sentry/bundler-plugin-core/node_modules/minimatch/node_modules/balanced-match": {
-      "version": "1.0.2",
-      ...
-    },
-    "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",
-      "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
-      "license": "MIT",
-      "dependencies": {
-        "balanced-match": "^1.0.0"
-      }
-    },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/balanced-match": {
-      "version": "1.0.2",
-      ...
-    },
-    "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch/node_modules/brace-expansion": {
-      "version": "2.1.2",
-      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
-      "integrity": "sha512-w5JZcKgdhDOgOwm8H+K...",
-      ...
-    },

After (safe): These blocks are simply absent. npm now resolves brace-expansion for these paths to one of the patched releases: 1.1.17, 2.1.3, 3.0.3, or 5.0.8 — all of which include the fix that limits expansion output size.

Why Removing the Nested Entry Is the Right Fix

npm's lockfile nests a private copy of a package when it cannot satisfy a version range using an already-installed ancestor. By removing the nested pin, we allow npm to hoist the dependency resolution upward and reuse a patched version that already satisfies the semver range. The consuming code in minimatch doesn't care which patch version of brace-expansion it gets — it only requires ^2.0.0 or similar — so using 2.1.3 instead of 2.1.2 is a fully backward-compatible substitution.

Optionally Enforcing the Fix with npm Overrides

For extra safety, you can add a top-level overrides block to package.json to force all transitive consumers to use the patched version, even if future installs re-introduce a nested copy:

{
  "overrides": {
    "brace-expansion": "^2.1.3"
  }
}

This acts as a belt-and-suspenders measure alongside the lockfile fix.


Key Takeaways

  • Nested package-lock.json entries can pin vulnerable versions invisibly. The two brace-expansion@2.1.2 copies were hidden four levels deep under @sentry/bundler-plugin-core and @typescript-eslint/typescript-estree — invisible to npm outdated and easy to miss in manual review.
  • Removing a pinned nested entry is often safer than patching it in place. Deleting the block lets npm's resolver find the best compatible patched version automatically, rather than requiring you to manually craft the correct integrity hash.
  • Transitive dev dependencies are still attack surface. Both vulnerable copies lived under tooling packages (@sentry/bundler-plugin-core, @typescript-eslint/typescript-estree). If these tools run in environments that process untrusted input — like CI pipelines that lint contributor PRs — the DoS risk is real.
  • brace-expansion's ^2.1.2 semver range allowed a safe in-place upgrade to 2.1.3 without any API changes, demonstrating why semantic versioning patch releases exist.
  • Trivy's filesystem scan mode (trivy fs .) catches lockfile vulnerabilities that SAST tools focused on source code would miss entirely.

How Orbis AppSec Detected This

  • Source: The vulnerable brace-expansion@2.1.2 package version, as recorded in two nested entries within package-lock.json, constitutes the tainted dependency.
  • Sink: Any call path in @sentry/bundler-plugin-core or @typescript-eslint/typescript-estree that invokes minimatch() with externally influenced pattern strings ultimately reaches the unguarded expansion loop inside brace-expansion/index.js.
  • Missing control: No upper bound on the number of expanded strings; no input length or complexity validation before invoking the expander.
  • CWE: CWE-400 — Uncontrolled Resource Consumption.
  • Fix: Removed the two pinned brace-expansion@2.1.2 nested entries (and their associated balanced-match@1.0.2 copies) from package-lock.json, allowing npm to resolve both paths to a patched release (≥2.1.3).

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-14257 is a reminder that the real attack surface of a modern Node.js application is not just the code you write — it is the entire transitive dependency graph locked in package-lock.json. A single two-line package entry, nested four levels deep under a Sentry bundler plugin, was enough to expose the application to CPU exhaustion attacks. The fix required removing fewer than 30 lines from the lockfile, but finding those lines required a dedicated scanner that understands nested dependency resolution.

Make vulnerability scanning of your lockfile a first-class citizen in your CI pipeline. Treat package-lock.json as security-relevant configuration, not just build plumbing. And when a scanner flags a nested copy of a package, don't dismiss it as "not directly reachable" — trace the call path, understand the risk, and remove the pin.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2173

Related Articles

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

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 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.