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

critical

How Denial of Service via gzip bomb happens in Node.js tar and how to fix it

CVE-2026-59873 is a critical Denial of Service vulnerability in the `node-tar` package (versions before 7.5.19) that allows an attacker to trigger resource exhaustion by supplying a crafted gzip bomb archive. The fix upgrades `tar` from 7.5.16 to 7.5.19 in both `package.json` and `package-lock.json`, closing the attack surface for any Node.js application that processes tar archives. Because this package is used in production code — not just in tests — the exposure was real and immediate.

high

How Nodemailer raw option bypass happens in Node.js and how to fix it

A high-severity vulnerability in Nodemailer versions prior to 9.0.1 allowed attackers to bypass the `disableFileAccess` and `disableUrlAccess` security controls using the message-level `raw` option. This bypass enabled arbitrary file reads from the server and full-response Server-Side Request Forgery (SSRF) attacks, potentially exposing sensitive configuration files and internal network resources. The fix involves upgrading Nodemailer from version 8.0.7 to 9.0.1.

critical

How unsafe random function in form-data happens in Node.js and how to fix it

The `form-data` npm package used `Math.random()` — a cryptographically unsafe pseudo-random number generator — to generate multipart form boundaries. This critical vulnerability (CVE-2025-7783) allowed attackers to predict boundary strings and potentially inject malicious content into multipart requests. The fix upgrades `form-data` from version 2.3.3 to 4.0.4, which uses a cryptographically secure random source.

high

How Denial of Service via malformed HTTP header decoding happens in OpenTelemetry JavaScript and how to fix it

A high-severity Denial of Service vulnerability (CVE-2026-59892) was discovered in @opentelemetry/propagator-jaeger version 2.7.1, where malformed HTTP headers could crash Node.js applications during trace context decoding. The vulnerability was fixed by upgrading from version 2.7.1 to 2.9.0, which includes improved header validation and error handling to prevent application crashes from malicious or corrupted trace propagation headers.

critical

How remote code execution happens in Node.js remote-run.js and how to fix it

A critical remote code execution vulnerability in `utils/remote-run.js` allowed attackers to execute arbitrary JavaScript code by passing any malicious URL to the script. The fix implements URL whitelist validation to ensure only trusted sources can be executed, preventing complete system compromise on any machine running the vulnerable code.

high

How secrets: inherit over-privilege happens in GitHub Actions reusable workflows and how to fix it

A high-severity security finding was identified in `templates/claude-workflow/workflows/claude.yml` where `secrets: inherit` passed every repository secret to a reusable workflow, violating the principle of least privilege. The fix explicitly passes only `CLAUDE_CODE_OAUTH_TOKEN`—the single secret the called workflow actually needs—drastically reducing the blast radius if the reusable workflow is ever compromised.