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"inpackage.json - npm:
"overrides"inpackage.json(npm 8.3+) - pnpm:
"pnpm.overrides"inpackage.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
- OWASP: A06:2021 – Vulnerable and Outdated Components
- CWE-400: Uncontrolled Resource Consumption
- CWE-1333: Inefficient Regular Expression Complexity (related pattern)
Key Takeaways
brace-expansion@2.1.4inyarn.lockwas the vulnerable artifact — not any application code. Lock file scanning is essential;package.jsonscanning alone would have missed this.- A Yarn
resolutionsoverride 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.0was a necessary side effect of the upgrade — the2.xseries ofbrace-expansionusedbalanced-match@1.x, while5.xuses4.x. Both old entries were safely removed fromyarn.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-expansionpackage's expansion function, invoked transitively throughgloborminimatch, resolving to the vulnerablebrace-expansion@2.1.4entry inyarn.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 theresolutionsfield inpackage.jsonand regeneratedyarn.lockto remove the vulnerable2.1.4entry and itsbalanced-match@1.xcompanion
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.