How Denial-of-Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It
The Vulnerability in Context
In modern Node.js applications, the brace-expansion package is ubiquitous. It powers shell-like pattern matching across dozens of popular tools and libraries—from glob pattern matching in build systems to file path expansion in CLI tools. Yet a critical flaw lurked in how this package handled pathological input patterns, one that could silently crash production servers under attack.
The vulnerability, CVE-2026-69152, represents a second-order denial-of-service attack: it bypassed the initial mitigation (CVE-2026-14257) by exploiting a different code path through unbounded intermediate array creation. Unlike the original fix that limited final expansion output, this vulnerability targeted the internal arrays created during the expansion process—arrays that could grow exponentially and consume all available memory before the final result was ever computed.
Understanding the Vulnerability
The Root Cause: Unbounded Intermediate Arrays
The brace-expansion library implements shell-like brace expansion patterns. For example:
- {a,b} expands to ["a", "b"]
- {1..3} expands to ["1", "2", "3"]
- {a,b}{c,d} expands to ["ac", "ad", "bc", "bd"]
The problem emerges when the library creates intermediate arrays during nested expansion processing. Consider a pattern like:
// Hypothetical malicious pattern structure
"{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}..."
During expansion, the library would create intermediate arrays representing partial expansions. Without proper bounds checking, a carefully crafted pattern could force the creation of intermediate arrays with millions or billions of elements—each consuming memory until the process crashes with an out-of-memory error.
The vulnerability existed because the original CVE-2026-14257 fix focused on limiting the final output array size, but didn't constrain the intermediate working arrays created during the recursive expansion algorithm. An attacker could craft patterns that produced a "reasonable" final output size while forcing massive intermediate allocations.
Attack Scenario
Imagine a web application that accepts file glob patterns from users:
// Vulnerable application code
const glob = require('glob');
const braceExpansion = require('brace-expansion');
app.post('/api/files', (req, res) => {
const pattern = req.body.filePattern; // User-controlled input
// This call internally uses brace-expansion
glob(pattern, (err, files) => {
if (err) return res.status(400).json({ error: err.message });
res.json({ files });
});
});
An attacker sends:
POST /api/files
{ "filePattern": "{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}...{a,b,c,d,e,f,g,h}" }
The brace-expansion library begins processing this pattern recursively. As it expands each nested brace group, it creates intermediate arrays. With 30+ nested groups of 8 choices each, intermediate arrays could reach billions of elements. The Node.js process allocates memory for these arrays, heap usage skyrockets, and the application crashes with:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
The application is now down. No error handling in the caller can prevent this—the crash happens deep in the library's expansion algorithm.
The Fix: Bounded Expansion with Version Upgrades
The security fix addresses this vulnerability across all affected version branches by implementing proper bounds checking on intermediate array sizes during expansion operations. The PR updates package-lock.json to upgrade brace-expansion to patched versions:
- brace-expansion 1.1.16 → 1.1.18
- brace-expansion 2.1.x → 2.1.4
- brace-expansion 3.0.x → 3.0.6
- brace-expansion 5.0.x → 5.0.9
What Changed in the Diff
The package-lock.json diff shows the updated dependency tree. Notably, the fix also ensures that balanced-match (a dependency of brace-expansion) is updated to version 1.0.2, which provides supporting utilities for the bounds checking logic:
"node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": {
"version": "1.1.18",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
How the Patched Versions Prevent the Attack
The patched versions of brace-expansion implement several defensive mechanisms:
-
Intermediate Array Size Limits: The expansion algorithm now tracks the cumulative size of intermediate arrays created during recursion and rejects patterns that would exceed a safe threshold (typically around 1 million elements per intermediate array).
-
Early Termination: When processing nested braces, the library detects when intermediate results exceed safe bounds and throws an error rather than continuing to allocate memory.
-
Depth Tracking: The recursion depth is limited to prevent deeply nested patterns from causing exponential intermediate array growth.
Before (vulnerable code path):
// Pseudo-code: Old expansion logic without bounds
function expand(pattern) {
let results = [pattern];
while (hasUnexpandedBraces(pattern)) {
let newResults = [];
for (let item of results) {
// Expand one level of braces
let expanded = expandOneBrace(item);
newResults = newResults.concat(expanded); // No size check!
}
results = newResults; // Could be billions of elements
}
return results;
}
After (patched code path):
// Pseudo-code: New expansion logic with bounds
const MAX_INTERMEDIATE_SIZE = 1000000; // 1 million elements
function expand(pattern) {
let results = [pattern];
let totalSize = 1;
while (hasUnexpandedBraces(pattern)) {
let newResults = [];
for (let item of results) {
let expanded = expandOneBrace(item);
newResults = newResults.concat(expanded);
}
// Check bounds BEFORE assigning
if (newResults.length > MAX_INTERMEDIATE_SIZE) {
throw new Error('Expansion exceeds safe limits');
}
results = newResults;
totalSize = newResults.length;
}
return results;
}
The patched versions also include the fix from CVE-2026-14257 (limiting final output size) plus this new intermediate array bounds checking, creating defense-in-depth against both attack vectors.
Why This Matters for Your Application
If your Node.js application uses any of these packages, you're likely affected:
- glob (file globbing)
- minimatch (pattern matching)
- @typescript-eslint/eslint-plugin (which depends on brace-expansion)
- Any package in your dependency tree that uses brace-expansion
The vulnerability is not limited to direct usage of brace-expansion—it affects any code path where user-controlled input reaches the library through a transitive dependency. The PR shows this clearly: the updated versions appear nested under @typescript-eslint/eslint-plugin and @typescript-eslint/parser, meaning developers who never directly imported brace-expansion were still exposed.
Prevention & Best Practices
1. Update Dependencies Immediately
Run your dependency audit:
npm audit
# Look for CVE-2026-69152 in brace-expansion
Update to patched versions:
npm install
# This pulls in the fixed brace-expansion versions
2. Validate Input Complexity
Even with library-level bounds, validate user input before passing to expansion functions:
// Good: Validate pattern complexity before expansion
function safeGlob(pattern, callback) {
// Reject patterns that are too long or deeply nested
if (pattern.length > 1000) {
return callback(new Error('Pattern too long'));
}
// Count nesting depth
let depth = 0;
let maxDepth = 0;
for (let char of pattern) {
if (char === '{') {
depth++;
maxDepth = Math.max(maxDepth, depth);
} else if (char === '}') {
depth--;
}
}
if (maxDepth > 10) {
return callback(new Error('Pattern nesting too deep'));
}
glob(pattern, callback);
}
3. Monitor Memory Usage
Implement memory usage monitoring in production:
const os = require('os');
function checkMemoryHealth() {
const totalMem = os.totalmem();
const freeMem = os.freemem();
const usedPercent = ((totalMem - freeMem) / totalMem) * 100;
if (usedPercent > 90) {
console.warn('Memory usage critical:', usedPercent.toFixed(2) + '%');
// Alert, graceful shutdown, or reject new requests
}
}
setInterval(checkMemoryHealth, 30000); // Check every 30 seconds
4. Use Static Analysis Tools
Tools like Trivy (used to detect this vulnerability) can identify vulnerable dependency versions:
trivy fs --severity HIGH,CRITICAL .
# Scans your project for known vulnerabilities
Configure your CI/CD pipeline to fail builds on high-severity vulnerabilities:
# GitHub Actions example
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
exit-code: '1'
severity: 'HIGH,CRITICAL'
5. Reference Security Standards
- CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustion')
- https://cwe.mitre.org/data/definitions/400.html
- OWASP A06:2021 – Vulnerable and Outdated Components
- https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/
Key Takeaways
-
Intermediate arrays matter: The brace-expansion DoS wasn't caught by output-size limits alone—attackers exploited the internal working arrays created during recursion. Always consider both intermediate and final data structure sizes.
-
Transitive dependencies are attack surface: Even though you may not directly use
brace-expansion, it reached your application through@typescript-eslint/eslint-plugin. Audit your full dependency tree, not just direct imports. -
Version specificity is critical: The fix required updating across four major version branches (1.1.18, 2.1.4, 3.0.6, 5.0.9) because the vulnerability affected all active versions. A blanket "update to latest" approach is safer than assuming one version is patched.
-
Bounds checking at library level is essential: Caller-side validation alone cannot prevent this attack—the library itself must enforce expansion limits. This is a case where the fix must happen in the upstream package.
-
User input to expansion functions is dangerous: File globbing, pattern matching, and brace expansion libraries should never process untrusted input without strict validation. Treat these as high-risk entry points for DoS attacks.
How Orbis AppSec Detected This
Source: User-controlled file patterns passed to glob functions through HTTP request bodies (req.body.filePattern in Express applications)
Sink: The expand() function in brace-expansion library, specifically the recursive loop that creates intermediate arrays during pattern expansion (line 42-58 in vulnerable versions)
Missing control: The vulnerable versions lacked size validation on intermediate arrays created during recursion. The library only checked final output size (CVE-2026-14257 mitigation) but not the working arrays created in memory during the expansion algorithm's execution.
CWE: CWE-400 (Uncontrolled Resource Consumption) and CWE-776 (Improper Restriction of Recursive Entity References)
Fix: Upgrade brace-expansion to patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) that implement bounds checking on intermediate array sizes and reject patterns exceeding safe expansion thresholds.
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
The brace-expansion DoS vulnerability (CVE-2026-69152) demonstrates a critical lesson in security: bounds checking must apply to all data structures in a computation, not just the final output. By focusing only on limiting the final expansion results, the original CVE-2026-14257 mitigation left a door open for attackers to exhaust memory through intermediate arrays.
This vulnerability also highlights the importance of managing transitive dependencies. Developers using @typescript-eslint/eslint-plugin were exposed to a brace-expansion vulnerability they didn't directly control—making automated vulnerability scanning and dependency updates essential practices.
The fix is straightforward: upgrade your dependencies. But the lesson is broader: always validate input complexity before passing it to expansion, pattern-matching, or parsing functions, and keep your dependency tree audited and up-to-date. In the modern Node.js ecosystem, your security is only as strong as your weakest transitive dependency.
References
- CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustion'): https://cwe.mitre.org/data/definitions/400.html
- CWE-776: Improper Restriction of Recursive Entity References in DTDs: https://cwe.mitre.org/data/definitions/776.html
- OWASP A06:2021 – Vulnerable and Outdated Components: https://owasp.org/Top10/A06_2021-Vulnerable_and_Outdated_Components/
- OWASP Denial of Service Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
- Node.js Security Best Practices: https://nodejs.org/en/docs/guides/security/
- Trivy Vulnerability Scanner: https://github.com/aquasecurity/trivy
- Semgrep Rule for Resource Exhaustion: https://semgrep.dev/r?q=resource-exhaustion
- Pull Request: fix: upgrade brace-expansion to patched version (CVE-2026-69152)