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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #304

Related Articles

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 dependabot-missing-cooldown happens in GitHub Actions/Node.js and how to fix it

The repository's `.github/dependabot.yml` had no cooldown period configured, meaning Dependabot could immediately propose updates to newly published package versions with zero time for the community to flag malware or instability. The fix adds a `cooldown` block with `default-days: 7` to both the `npm` and `github-actions` ecosystems, forcing a 7-day waiting period before new releases are surfaced as update PRs.

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.