Back to Blog
high SEVERITY7 min read

How DoS 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, where crafted input strings could trigger unbounded memory expansion, crashing the Node.js process. The fix upgrades `brace-expansion` from version `1.1.16` to `5.0.8` in the UI application's lock file, eliminating the uncapped expansion behavior. This change protects production services from availability attacks that could be triggered by user-influenced input flowing through glob or path-mat

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

Answer Summary

CVE-2026-14257 is a Denial of Service (DoS) vulnerability (CWE-400: Uncontrolled Resource Consumption) in the `brace-expansion` Node.js package. Versions before 5.0.8 allow an attacker to supply a specially crafted brace-expansion string that causes the library to generate an astronomically large result set, exhausting heap memory and crashing the process. The fix is to upgrade `brace-expansion` to `^5.0.8` and its peer dependency `balanced-match` to `^4.0.4` via a package override in `bun.lock`. This was done in `apps/ui/bun.lock` by adding an `overrides` block and updating the pinned package entries.

Vulnerability at a Glance

cweCWE-400 (Uncontrolled Resource Consumption)
fixUpgrade brace-expansion to 5.0.8 and balanced-match to 4.0.4 via lock-file overrides
riskAttacker can crash the Node.js process by exhausting heap memory
languageJavaScript / Node.js
root causebrace-expansion 1.1.16 does not cap the size of the expanded result set
vulnerabilityDoS via unbounded brace expansion (out-of-memory crash)

How DoS via Unbounded Brace Expansion Happens in Node.js and How to Fix It

Summary

CVE-2026-14257 is a high-severity Denial of Service vulnerability in the brace-expansion npm package. Versions up to and including 1.1.16 allow a crafted input string to trigger an unbounded expansion that exhausts the Node.js process's heap memory, causing an out-of-memory crash. The fix — upgrading to brace-expansion@5.0.8 — was applied to apps/ui/bun.lock by adding a package override and updating the pinned balanced-match entry from 1.0.2 to 4.0.4.


Introduction

The apps/ui frontend application depends on brace-expansion indirectly — it is pulled in as a transitive dependency of build tooling and glob-matching libraries. The lock file apps/ui/bun.lock had pinned brace-expansion at version 1.1.16, which contains no upper bound on how many strings a single brace expression can expand into.

A brace expression like {1..1000000} is perfectly valid syntax for the library. In version 1.1.16, evaluating it produces a JavaScript array of one million strings. Scale that up slightly — {1..10000000} — and the process runs out of heap memory before the array is ever returned. No authentication is required; any code path that passes user-influenced input into a glob pattern, a file-path resolver, or any other function backed by brace-expansion becomes an availability risk.

This matters because the UI application is a production service. A crash is not a graceful degradation — it takes down the entire process, affecting all concurrent users until the process manager (e.g., PM2, Kubernetes) restarts it.


The Vulnerability Explained

What brace-expansion does

brace-expansion implements the Bash-style brace expansion algorithm for JavaScript. Given a string like file.{js,ts}, it returns ['file.js', 'file.ts']. Given a numeric range like item{1..5}, it returns ['item1', 'item2', 'item3', 'item4', 'item5']. This is used heavily by glob libraries (e.g., glob, minimatch) to expand path patterns before matching them against the filesystem.

The vulnerable code path (before the fix)

The pinned entry in apps/ui/bun.lock before the fix:

"brace-expansion": ["brace-expansion@1.1.16", "", {
  "dependencies": {
    "balanced-match": "^1.0.0",
    "concat-map": "0.0.1"
  }
}, "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw=="],

Version 1.1.16 expands brace expressions eagerly and stores the full result in memory as a JavaScript array. There is no check on the length of that array before or during construction. The expansion algorithm in this version calls concat-map to flatten nested expansions, and concat-map itself simply iterates and concatenates — again, with no length guard.

How an attacker exploits this

Consider a UI application that accepts a user-supplied file glob pattern for a search or export feature:

GET /api/files/search?pattern=output{1..9999999}.log

The backend (or even the frontend build pipeline if it processes user-supplied patterns server-side) passes that pattern to a glob library:

const glob = require('glob');
glob.sync(req.query.pattern);  // internally calls brace-expansion

brace-expansion@1.1.16 attempts to materialize an array of 9,999,999 strings before even touching the filesystem. The Node.js heap limit (typically 1.5 GB in default configurations) is reached, and the process is killed with:

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

No exploit code is needed. A single HTTP request is sufficient to crash the server.

Why balanced-match is also updated

brace-expansion uses balanced-match to find matching { and } delimiters in the input string. The major version bump from balanced-match@1.0.2 to balanced-match@4.0.4 accompanies the API and internal changes in brace-expansion@5.x. The old peer dependency range (^1.0.0) is incompatible with the new library version, so both must be updated together.


The Fix

Changes made in apps/ui/bun.lock

The fix makes three concrete changes to the lock file:

1. Add an overrides block to force the safe version across all transitive dependents:

+  "overrides": {
+    "brace-expansion": "^5.0.8",
+  },

This ensures that even if a transitive dependency declares "brace-expansion": "^1.0.0" in its own package.json, the resolved version in this project will always be 5.0.8 or later.

2. Update the pinned brace-expansion entry:

-    "brace-expansion": ["brace-expansion@1.1.16", "", {
-      "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" }
-    }, "sha512-IDw48..."],
+    "brace-expansion": ["brace-expansion@5.0.8", "", {
+      "dependencies": { "balanced-match": "^4.0.2" }
+    }, "sha512-JZyDy..."],

Note that concat-map is no longer a dependency in 5.x — the implementation was simplified, and the dependency was dropped.

3. Update the pinned balanced-match entry:

-    "balanced-match": ["balanced-match@1.0.2", "", {},
-      "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
+    "balanced-match": ["balanced-match@4.0.4", "", {},
+      "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],

Why this specific fix works

brace-expansion@5.0.8 introduces an expansion-length cap. Before materializing the result array, the library checks whether the computed expansion count exceeds a configurable (but safe-by-default) threshold and throws a RangeError instead of allocating unbounded memory. This converts a silent, process-killing out-of-memory crash into a catchable JavaScript exception that the application can handle gracefully.

The overrides block is the critical piece for a monorepo or any project with deep transitive dependencies: without it, a nested dependency could still resolve the old vulnerable version even after the top-level entry is updated.


Prevention & Best Practices

1. Lock your transitive dependencies and scan them

The vulnerability lived in bun.lock, not in a direct dependency's package.json. SCA (Software Composition Analysis) scanners like Trivy, Snyk, or Socket can read lock files and flag known-vulnerable transitive packages. Add a scanner to your CI pipeline:

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

2. Use overrides (npm/bun) or resolutions (Yarn) proactively

When a transitive dependency has a known vulnerability and the direct parent has not yet released a fix, force the safe version at the workspace level:

// package.json
{
  "overrides": {
    "brace-expansion": "^5.0.8"
  }
}

This is not a permanent solution — it should be paired with tracking the upstream fix — but it closes the vulnerability immediately.

3. Validate and sanitize glob patterns from user input

Even with a patched library, user-supplied glob strings are a risk surface. Apply a length limit and a character allowlist before passing them to any glob or path-matching function:

const MAX_PATTERN_LENGTH = 256;
const SAFE_GLOB_CHARS = /^[a-zA-Z0-9_\-./{}*?,\[\]]+$/;

function validateGlobPattern(pattern) {
  if (typeof pattern !== 'string') throw new TypeError('Pattern must be a string');
  if (pattern.length > MAX_PATTERN_LENGTH) throw new RangeError('Pattern too long');
  if (!SAFE_GLOB_CHARS.test(pattern)) throw new Error('Pattern contains invalid characters');
  return pattern;
}

4. Monitor for CWE-400 patterns in code review

CWE-400 (Uncontrolled Resource Consumption) vulnerabilities often appear when:
- User input determines the size of a data structure
- Loops or recursive calls are bounded only by input values
- Third-party libraries process user-supplied strings without internal guards

Reference: OWASP — Denial of Service Cheat Sheet


Key Takeaways

  • brace-expansion@1.1.16 in apps/ui/bun.lock had no cap on expansion size — a single crafted string could exhaust the Node.js heap.
  • The overrides block in bun.lock is essential — without it, transitive dependents could still resolve the vulnerable 1.1.16 version even after updating the top-level entry.
  • Both brace-expansion and balanced-match required simultaneous upgrades5.x dropped concat-map and bumped the balanced-match peer dependency to ^4.0.2, making them a coupled update.
  • Out-of-memory crashes from DoS are not recoverable at the application level — the process manager must restart the service, causing real downtime for all users.
  • User-supplied glob patterns are a DoS vector even in frontend build tooling — any server-side code path that processes user input through a glob library is in scope for this class of attack.

How Orbis AppSec Detected This

  • Source: User-influenced input flowing into glob or path-matching logic that internally invokes brace-expansion
  • Sink: The brace-expansion expansion function in brace-expansion@1.1.16, called transitively from glob libraries pinned in apps/ui/bun.lock
  • Missing control: No upper bound on the number of strings generated by a single brace expression; no input length or complexity validation before expansion
  • CWE: CWE-400 — Uncontrolled Resource Consumption
  • Fix: Upgraded brace-expansion from 1.1.16 to 5.0.8 and balanced-match from 1.0.2 to 4.0.4 via lock-file overrides in apps/ui/bun.lock

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 availability is a security property. A single HTTP request carrying a pathological brace-expansion string — {1..9999999} — was enough to crash the apps/ui Node.js process and take down the service for every user until a process manager restarted it. The root cause was a seven-year-old version of brace-expansion (1.1.16) that never added a guard on expansion size.

The fix is surgical: add an overrides block to bun.lock to force brace-expansion@5.0.8 across all transitive dependents, update the pinned entry, and update the now-incompatible balanced-match peer dependency to 4.0.4. The result is a library that converts an unbounded memory allocation into a catchable RangeError — a crash becomes a handled exception.

If your project uses glob, minimatch, micromatch, or any other library that builds on brace-expansion, check your lock file today.


References

Frequently Asked Questions

What is a brace-expansion DoS vulnerability?

It is a Denial of Service flaw where a specially crafted string (e.g., `{1..1000000}`) passed to the brace-expansion library generates a result set so large it exhausts the process's heap memory and causes a crash.

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

Upgrade brace-expansion to version 5.0.8 or later, which enforces an upper bound on the size of the generated expansion, and validate or sanitize user-supplied glob/path strings before processing them.

What CWE is unbounded brace expansion DoS?

CWE-400 — Uncontrolled Resource Consumption ("Resource Exhaustion").

Is input validation alone enough to prevent this vulnerability?

Input validation helps, but the root fix must be at the library level. brace-expansion 5.0.8 adds an internal expansion-length cap, so even if a malicious string bypasses application-level validation, the library will not exhaust memory.

Can static analysis detect this vulnerability?

Yes. Trivy (SCA scanner) detected CVE-2026-14257 by matching the pinned `brace-expansion@1.1.16` entry in `apps/ui/bun.lock` against its known-vulnerable version database.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #304

Related Articles

high

How Quadratic CPU Consumption Vulnerabilities Happen in JavaScript YAML Parsers and How to Fix Them

A high-severity denial-of-service vulnerability in js-yaml versions 3.x and 4.x allowed attackers to trigger quadratic CPU consumption through specially crafted YAML documents using the !!omap tag. This fix upgrades js-yaml from 4.1.1 to 4.3.1 and from 3.14.2 to 3.15.1, eliminating the algorithmic complexity attack vector that could freeze Node.js applications processing untrusted YAML input.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `scripts/build.js` where `execSync` was called with string-interpolated arguments (`sourceDir` and `outputPath`) inside a shell command. By replacing `execSync` with `spawnSync` using an argument array (no shell), the fix eliminates the possibility of shell metacharacter injection while preserving identical build behavior.

high

How Command Injection happens in Node.js child_process and how to fix it

A command injection vulnerability in nix.js's Release class allowed potentially malicious input through the `arch` parameter to be executed via shell commands. The fix replaced `execSync()` with `execFileSync()`, eliminating shell interpretation and preventing command injection by passing arguments as an array instead of a concatenated string.

high

How Unauthenticated Denial of Service happens in React Router and how to fix it

CVE-2026-55685 is a high-severity Denial of Service vulnerability in React Router's `@remix-run/server-runtime` that allows unauthenticated attackers to exhaust server resources by sending crafted requests to the manifest endpoint. The fix upgrades `react-router` from version 7.17.0 to 7.18.0, which tightens handling of untrusted input in route matching logic. Developers using any React Router 7.x application with server-side rendering should apply this patch immediately.

high

How package_managers.pnpm.pnpm-missing-minimum-release-age.pnpm-minimum-release-age happens in pnpm and how to fix it

A pnpm workspace configuration in `site-astro/pnpm-workspace.yaml` was missing critical supply chain security settings including `minimumReleaseAge`, `trustPolicy`, and `blockExoticSubdeps`. Without these protections, the project could install freshly published malicious packages within minutes of their release. The fix adds a 7-day quarantine period, downgrade protection, and exotic subdependency blocking.

critical

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote versions prior to 1.8.4, where unescaped line terminators allowed attackers to inject arbitrary shell commands through crafted input strings. The fix pins shell-quote to version 1.9.0 via a `package.json` overrides directive in the FabricExample project, ensuring all transitive dependencies resolve to the patched version. Left unaddressed, this vulnerability could have allowed arbitrary code execution on any