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.


Prevention & Best Practices

1. Run a Vulnerability Scanner in CI

Tools like Trivy, npm audit, Snyk, and Socket inspect package-lock.json (not just package.json) and will flag vulnerable nested copies that a simple npm outdated would miss. Add one of these to your CI pipeline as a required check.

# Example GitHub Actions step
- name: Run Trivy vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    scan-ref: '.'
    severity: 'HIGH,CRITICAL'
    exit-code: '1'

2. Use npm Overrides for Transitive Pinning

When a vulnerable package is deep in the dependency tree and you can't wait for upstream maintainers to release a fix, use overrides (npm ≥8) or resolutions (Yarn) to force a safe version across the entire tree.

3. Audit Your Lockfile Regularly

package-lock.json can grow stale. Run npm audit and npm dedupe periodically. The npm dedupe command collapses duplicate nested copies where semver ranges permit, reducing both attack surface and bundle size.

4. Validate Glob Inputs at the Application Layer

If your application accepts user-supplied glob or file-path patterns, validate them before passing to minimatch or similar libraries:

// Reject patterns with excessive brace nesting
function isSafeGlob(pattern) {
  const braceCount = (pattern.match(/\{/g) || []).length;
  if (braceCount > 10) throw new Error('Pattern too complex');
  return pattern;
}

This defense-in-depth measure protects you even if the underlying library has an unpatched vulnerability.

5. Reference Security Standards

  • OWASP A06:2021 — Vulnerable and Outdated Components directly addresses this scenario: transitive dependencies with known CVEs.
  • CWE-400: Uncontrolled Resource Consumption is the root-cause classification for this type of DoS.
  • NIST NVD entry for CVE-2026-14257 provides the official severity score and affected version range.

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.


References

Frequently Asked Questions

What is a denial-of-service vulnerability in brace-expansion?

brace-expansion parses shell-style brace patterns like `{a,b,c}` into lists of strings. In versions through 5.0.7, a specially crafted pattern can cause the expander to generate an astronomically large number of combinations, consuming all available CPU and memory and making the process unresponsive.

How do you prevent denial-of-service in Node.js dependency trees?

Pin or override transitive dependencies to their patched versions using npm `overrides` (npm ≥8) or Yarn `resolutions`, run `npm audit` or a scanner like Trivy in CI, and remove redundant nested dependency copies that lock old vulnerable versions.

What CWE is this denial-of-service vulnerability?

CWE-400 — Uncontrolled Resource Consumption. The program does not limit the resources consumed when processing a user-influenced input, allowing an attacker to exhaust CPU or memory.

Is upgrading the top-level package enough to prevent this?

Not always. As shown here, nested copies of `brace-expansion@2.1.2` were pinned inside `@sentry/bundler-plugin-core` and `@typescript-eslint/typescript-estree`, meaning a top-level upgrade would not replace them. You must also remove or override those nested entries.

Can static analysis detect this denial-of-service vulnerability?

Yes. Trivy flagged the exact nested entries in `package-lock.json` by matching the package version against its CVE database. Tools like `npm audit`, Snyk, and Dependabot can also surface transitive vulnerable copies.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #2173

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.