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.16inapps/ui/bun.lockhad no cap on expansion size — a single crafted string could exhaust the Node.js heap.- The
overridesblock inbun.lockis essential — without it, transitive dependents could still resolve the vulnerable1.1.16version even after updating the top-level entry. - Both
brace-expansionandbalanced-matchrequired simultaneous upgrades —5.xdroppedconcat-mapand bumped thebalanced-matchpeer 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-expansionexpansion function inbrace-expansion@1.1.16, called transitively from glob libraries pinned inapps/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-expansionfrom1.1.16to5.0.8andbalanced-matchfrom1.0.2to4.0.4via lock-file overrides inapps/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.