Back to Blog
high SEVERITY8 min read

How Denial of Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It

CVE-2026-14257 is a high-severity denial-of-service vulnerability in the `brace-expansion` npm package (versions through 5.0.7) that allows an attacker to trigger an out-of-memory process crash by supplying a crafted string with deeply nested or exponentially large brace patterns. The fix upgrades the dependency to version 5.0.8 and pins it via a Yarn resolution to ensure no transitive dependency pulls in the vulnerable version. Left unpatched, this vulnerability could be exploited to take down

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

Answer Summary

CVE-2026-14257 is a Denial of Service (DoS) vulnerability (CWE-400: Uncontrolled Resource Consumption) in the `brace-expansion` npm package through version 5.0.7. When the library processes a specially crafted brace-expansion string (e.g., `{a,b,c,...}` with exponential nesting), it generates an unbounded number of expansion results, exhausting process memory and crashing the Node.js application. The fix upgrades `brace-expansion` to 5.0.8 in `package.json` using a Yarn `resolutions` override, ensuring all transitive dependencies receive the patched version. Developers should pin vulnerable transitive dependencies using package manager resolution overrides and re-scan after upgrades to confirm remediation.

Vulnerability at a Glance

cweCWE-400 (Uncontrolled Resource Consumption)
fixUpgrade brace-expansion to 5.0.8 and add a Yarn resolutions override to enforce the patched version across all transitive dependencies
riskAttacker can crash the Node.js process with a single crafted request, causing full service unavailability
languageJavaScript / Node.js
root causebrace-expansion ≤5.0.7 does not limit the total number of strings generated from a brace pattern, allowing exponential memory growth
vulnerabilityDenial of Service via Unbounded Brace Expansion

How Denial of Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It

Introduction

The yarn.lock file in this web application contained a silent time bomb: brace-expansion@2.1.4, a transitive dependency quietly pulled in by other packages in the dependency tree. This version is vulnerable to CVE-2026-14257, a high-severity denial-of-service flaw where a single crafted string can exhaust all available process memory and crash the Node.js server — no authentication required.

What makes this class of vulnerability particularly dangerous is its invisibility. No application code references brace-expansion directly. It lives two or three levels deep in the dependency graph, hidden inside packages like glob and minimatch. Without a lock-file scanner, it would never surface in a code review.


The Vulnerability Explained

What Is Brace Expansion?

Brace expansion is a shell feature that transforms a pattern like {a,b,c} into the list ['a', 'b', 'c'], or file{1..5}.txt into ['file1.txt', 'file2.txt', ..., 'file5.txt']. The brace-expansion npm package implements this behavior for JavaScript, and it is a foundational dependency for glob-pattern matching in the Node.js ecosystem.

The expansion is inherently multiplicative. The pattern {a,b}{c,d} produces 4 strings. {a,b}{c,d}{e,f} produces 8. Add a few more levels of nesting and the count grows exponentially.

The Root Cause: No Expansion Limit

In brace-expansion versions through 5.0.7 (including the 2.x series), there is no upper bound on the number of strings the library will generate. An attacker who can influence a brace-pattern string — directly via an API parameter, indirectly via a filename, glob pattern, or configuration value — can supply a payload like:

{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}{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}{a,b,c,d,e,f,g,h,i,j}{a,b,c,d,e,f,g,h,i,j}

This single string, when expanded, produces 10⁸ (100 million) entries. The library attempts to allocate all of them in memory simultaneously. On a typical Node.js process with a default heap of ~1.5 GB, this triggers a fatal out-of-memory crash.

What the Lock File Revealed

Trivy's scan of yarn.lock found two entries for brace-expansion: the vulnerable 2.x version pulled in as a transitive dependency, and a newer 5.x version. The vulnerable entry looked like this:

# yarn.lock (BEFORE — vulnerable)
"brace-expansion@npm:^2.0.2":
  version: 2.1.4
  resolution: "brace-expansion@npm:2.1.4"
  dependencies:
    balanced-match: "npm:^1.0.0"
  checksum: 10c0/6c0a0e2573eac1dc565b52b1e1bfbeba39bf1830d106ebbc61ff1eaefcf610e9111cd3baa091addd18e33292135c99e019d3184d966389d85dc958fdcdc1449f
  languageName: node
  linkType: hard

The ^2.0.2 semver range means any package in the dependency tree requesting brace-expansion@^2.x would receive the vulnerable 2.1.4. Because this is a web application that handles user-influenced input, the attack surface is real.

Attack Scenario

Consider a file-search or glob-matching feature in this web application. A user submits a search pattern that eventually passes through a glob() call. Internally, glob calls minimatch, which calls brace-expansion. The attacker's payload:

POST /api/search
{ "pattern": "{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p}{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p}{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p}{a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p}" }

This produces 16⁴ = 65,536 strings on the conservative end — but add two more groups and you're at 16⁶ = 16 million. The Node.js process runs out of heap memory, the crash is immediate, and the service is unavailable until it restarts. Because the crash is deterministic and reproducible, an attacker can loop this request to prevent recovery.


The Fix

The fix required changes to two files: package.json and yarn.lock.

Step 1: Force the Safe Version via Yarn Resolutions (package.json)

The core problem is that multiple packages in the dependency tree declare a ^2.x dependency on brace-expansion. Simply upgrading one package won't help — the vulnerable version will keep being installed for the others.

The solution is a Yarn resolution override, which forces every package in the tree to use the specified version, regardless of what they individually request:

// package.json (AFTER — safe)
"resolutions": {
  "@hono/node-server": "^2.0.11",
  "brace-expansion": "^5.0.8",   // ← added
  "glob": "^10.5.0",
  "js-yaml": "^4.3.1",
  "postcss@npm:8.4.31": "npm:8.5.23",
  ...
}

This single line ensures that no matter which package requests brace-expansion, Yarn will resolve it to 5.0.8 or later — a version that includes the fix.

Step 2: Remove the Vulnerable Lock File Entry (yarn.lock)

With the resolution override in place, the yarn.lock file was regenerated. The vulnerable 2.1.4 entry and its companion balanced-match@^1.0.0 dependency were removed entirely:

# yarn.lock (BEFORE — vulnerable entries removed)
-"balanced-match@npm:^1.0.0":
-  version: 1.0.2
-  resolution: "balanced-match@npm:1.0.2"
-  checksum: 10c0/9308baf0a7e4838a82bbfd11e01b1cb0f0cf2893bc1676c27c2a8c0e70cbae1c59120c3268517a8ae7fb6376b4639ef81ca22582611dbee4ed28df945134aaee
-  languageName: node
-  linkType: hard

-"brace-expansion@npm:^2.0.2":
-  version: 2.1.4
-  resolution: "brace-expansion@npm:2.1.4"
-  dependencies:
-    balanced-match: "npm:^1.0.0"
-  checksum: 10c0/6c0a0e2573eac1dc565b52b1e1bfbeba39bf1830d106ebbc61ff1eaefcf610e9111cd3baa091addd18e33292135c99e019d3184d966389d85dc958fdcdc1449f
-  languageName: node
-  linkType: hard

The updated entry now points all consumers to the safe version:

# yarn.lock (AFTER — safe)
"brace-expansion@npm:^5.0.8":
  version: 5.0.8
  resolution: "brace-expansion@npm:5.0.8"
  dependencies:
    balanced-match: "npm:^4.0.2"
  ...

Notice that balanced-match also moved from ^1.0.0 to ^4.0.2 — the older 1.x companion dependency is gone, and the 4.x version (already present in the lock file for other reasons) is now the sole entry.

Why This Specific Fix Works

Version 5.0.8 of brace-expansion introduces an internal limit on the total number of expansions the library will generate. When a pattern would produce more strings than the configured maximum, the library throws a controlled error instead of attempting to allocate unbounded memory. This converts a process-killing out-of-memory crash into a catchable exception — a vastly better failure mode.


Prevention & Best Practices

1. Scan Lock Files, Not Just package.json

Vulnerabilities like CVE-2026-14257 live in transitive dependencies that never appear in package.json. Tools like Trivy, Snyk, npm audit, and Socket analyze the full dependency graph in lock files. Run these in CI on every pull request.

# Example: scan with Trivy
trivy fs --scanners vuln .

# Example: npm audit
npm audit --audit-level=high

2. Use Resolution Overrides for Transitive Vulnerabilities

When a transitive dependency is vulnerable and the direct parent hasn't released a fix yet, use your package manager's override mechanism:

  • Yarn: "resolutions" in package.json
  • npm: "overrides" in package.json (npm 8.3+)
  • pnpm: "pnpm.overrides" in package.json
// npm overrides equivalent
"overrides": {
  "brace-expansion": "^5.0.8"
}

3. Validate User-Supplied Glob Patterns

If your application accepts glob or brace patterns from users, validate them before processing:

// Example: limit pattern complexity before expansion
const MAX_PATTERN_LENGTH = 256;

function safeGlob(userPattern) {
  if (userPattern.length > MAX_PATTERN_LENGTH) {
    throw new Error('Pattern too long');
  }
  // Count brace groups as a heuristic
  const braceGroups = (userPattern.match(/\{[^}]+\}/g) || []).length;
  if (braceGroups > 4) {
    throw new Error('Pattern too complex');
  }
  return glob(userPattern);
}

This defense-in-depth approach protects against both known and unknown expansion vulnerabilities.

4. Set Node.js Memory Limits

For web servers, consider setting a --max-old-space-size flag to limit how much memory the process can consume before Node.js throws a JavaScript OOM error rather than crashing the OS process. This won't prevent the DoS, but it may allow a graceful shutdown and restart:

node --max-old-space-size=512 server.js

5. Relevant Security Standards


Key Takeaways

  • brace-expansion@2.1.4 in yarn.lock was the vulnerable artifact — not any application code. Lock file scanning is essential; package.json scanning alone would have missed this.
  • A Yarn resolutions override is the correct tool for forcing a safe version of a transitive dependency when the direct parent hasn't yet updated its own dependency range.
  • Removing balanced-match@^1.0.0 was a necessary side effect of the upgrade — the 2.x series of brace-expansion used balanced-match@1.x, while 5.x uses 4.x. Both old entries were safely removed from yarn.lock.
  • Unbounded expansion is a multiplicative risk: even a modest brace pattern with 5 groups of 10 alternatives produces 100,000 strings. Application-level input validation on pattern length and complexity is a critical second line of defense.
  • This vulnerability is unauthenticated in web applications that pass any user-influenced string through a glob or file-matching path — the attack requires no credentials, session, or special privileges.

How Orbis AppSec Detected This

  • Source: User-influenced input (HTTP request parameters, filenames, or configuration values) that flow into glob or file-matching logic
  • Sink: The brace-expansion package's expansion function, invoked transitively through glob or minimatch, resolving to the vulnerable brace-expansion@2.1.4 entry in yarn.lock
  • Missing control: No upper bound on the number of strings generated during brace expansion; no application-level validation of pattern complexity before expansion
  • CWE: CWE-400 — Uncontrolled Resource Consumption
  • Fix: Added "brace-expansion": "^5.0.8" to the resolutions field in package.json and regenerated yarn.lock to remove the vulnerable 2.1.4 entry and its balanced-match@1.x companion

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 most dangerous dependencies are often the ones you never directly import. brace-expansion is a utility so small and so foundational that it rarely appears in architecture diagrams — yet a single unpatched version hiding in a lock file can take down an entire web service with a crafted HTTP request.

The fix here is precise and minimal: two files changed, one resolution override added, one vulnerable lock file entry removed. The application's behavior is unchanged; only the attack surface is reduced. That's the ideal security fix — surgical, verifiable, and non-disruptive.

Treat your lock files as security artifacts. Scan them in CI, pin vulnerable transitive dependencies with resolution overrides, and validate user-supplied patterns before they reach expansion libraries. The combination of automated scanning and targeted patching is what keeps production systems safe from vulnerabilities that would otherwise be invisible.


References

Frequently Asked Questions

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

It occurs when a library that expands shell-style brace patterns (e.g., `{a,b}{c,d}`) fails to cap the number of strings it generates. A crafted input with deeply nested or repeated braces can cause the expansion to produce billions of strings, consuming all available memory and crashing the process.

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

Upgrade to a patched version of brace-expansion (5.0.8+), use Yarn or npm resolutions/overrides to force the safe version across all transitive dependencies, and validate or sanitize user-supplied glob/brace patterns before passing them to expansion libraries.

What CWE is brace-expansion denial of service?

CWE-400: Uncontrolled Resource Consumption. The library consumes unbounded memory proportional to the size of the expanded output, with no internal limit to prevent exhaustion.

Is upgrading the direct dependency enough to prevent this vulnerability?

Not always. Because brace-expansion is often a transitive dependency (pulled in by packages like `glob` or `minimatch`), upgrading only the direct dependency may leave older versions installed elsewhere in the dependency tree. A package manager resolution override (Yarn `resolutions` or npm `overrides`) is required to enforce the safe version everywhere.

Can static analysis detect brace-expansion DoS vulnerabilities?

Yes. Software Composition Analysis (SCA) tools like Trivy, Snyk, and npm audit can flag known-vulnerable versions of brace-expansion in lock files. In this case, Trivy detected the vulnerable `brace-expansion@2.1.4` entry in `yarn.lock` and reported it as CVE-2026-14257.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #897

Related Articles

high

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.