Introduction
In a production application's dependency tree, Trivy scanner flagged a high-severity vulnerability in pnpm-lock.yaml: CVE-2026-69152 affecting the brace-expansion library. This wasn't just another routine dependency update—this vulnerability represented a sophisticated bypass of a previous security fix (CVE-2026-14257), demonstrating how attackers evolve their techniques to exploit the same underlying weakness through different code paths. The vulnerability allowed attackers to create unbounded intermediate arrays during brace expansion operations, potentially crashing applications or making them unresponsive through carefully crafted input patterns.
What makes this case particularly interesting is the complexity of the fix: the vulnerable library appeared multiple times in the dependency tree through different versions of minimatch (versions 3, 5, 9, and 10), requiring a coordinated upgrade strategy using pnpm's override mechanism to patch all instances simultaneously.
The Vulnerability Explained
The brace-expansion library is a fundamental JavaScript utility that expands brace notation patterns like {a,b,c} or {1..10} into arrays of strings. It's widely used through minimatch for glob pattern matching in file operations, build tools, and test runners. The library processes nested braces and ranges, generating intermediate arrays as it expands patterns.
CVE-2026-69152 exploits a weakness in how brace-expansion handles complex nested patterns. While CVE-2026-14257 added some bounds checking, attackers discovered they could still craft patterns that cause the library to generate massive intermediate arrays before any limits kick in. Consider this attack pattern:
// Malicious pattern that bypasses CVE-2026-14257 mitigation
const pattern = '{' + 'a,'.repeat(10000) + 'b}';
// Creates intermediate arrays during parsing phase
// before expansion limits are checked
Before the fix, vulnerable versions (1.1.13, 2.0.3, and 5.0.4) would process this pattern by:
- Parsing phase: Tokenizing the brace structure and creating intermediate arrays for each nesting level
- Expansion phase: Combining tokens into final expanded strings
- Result generation: Building the output array
The vulnerability occurs in step 1—the parsing phase creates unbounded intermediate arrays to hold tokens and partial results. Even if the final expansion is limited, the intermediate structures can consume gigabytes of memory. An attacker sending patterns like {{{{a,b},c},d},e} with deep nesting or {1..999999999} with huge ranges can trigger this behavior.
Real-World Attack Scenario
Imagine an application that uses minimatch for file filtering in a user-facing API:
// Vulnerable code pattern (conceptual)
app.post('/api/search-files', (req, res) => {
const pattern = req.body.pattern; // User-controlled input
const matcher = new Minimatch(pattern); // Uses brace-expansion internally
const results = files.filter(f => matcher.match(f));
res.json(results);
});
An attacker could POST a malicious pattern:
{
"pattern": "{a,b,c,d,e,f,g,h,i,j}{1..1000000}{x,y,z}"
}
This pattern would cause brace-expansion to:
- Create intermediate arrays for the first brace group (10 elements)
- Multiply by the range expansion (1,000,000 elements)
- Multiply by the final group (3 elements)
- Generate 30,000,000 intermediate strings before any limits apply
The Node.js process would consume all available memory, crash, or become unresponsive, denying service to legitimate users. Since minimatch is used in countless npm packages, build tools, and test frameworks, the attack surface is enormous.
The Fix
The fix required a sophisticated multi-version upgrade strategy because brace-expansion appears at different major versions throughout the dependency tree. Here's what changed in package.json:
"overrides": {
"@xmldom/xmldom": "^0.8.13",
"postcss": "8.5.10",
"uuid": "11.1.1",
"shell-quote": "1.9.0",
// NEW: Force specific brace-expansion versions for each minimatch version
"minimatch@3>brace-expansion": "1.1.18",
"minimatch@5>brace-expansion": "2.1.4",
"minimatch@9>brace-expansion": "2.1.4",
"minimatch@10>brace-expansion": "5.0.9"
}
And the corresponding changes in pnpm-lock.yaml:
Before (vulnerable versions):
brace-expansion@1.1.13:
resolution: {integrity: sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==}
brace-expansion@2.0.3:
resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==}
brace-expansion@5.0.4:
resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==}
After (patched versions):
brace-expansion@1.1.18:
resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==}
brace-expansion@2.1.4:
resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==}
brace-expansion@5.0.9:
resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8...}
Why This Specific Fix Works
The patched versions (1.1.18, 2.1.4, and 5.0.9) implement several critical improvements:
- Early bounds checking: Size limits are now enforced during the parsing phase, not just during final expansion
- Intermediate array limits: Maximum sizes are imposed on all temporary data structures
- Complexity detection: The parser now detects potentially malicious patterns (extreme nesting, huge ranges) and rejects them before processing
The fix required four separate overrides because:
- minimatch@3 uses brace-expansion 1.x → upgraded to 1.1.18
- minimatch@5 uses brace-expansion 2.x → upgraded to 2.1.4
- minimatch@9 uses brace-expansion 2.x → upgraded to 2.1.4
- minimatch@10 uses brace-expansion 5.x → upgraded to 5.0.9
Using pnpm's override mechanism ensures that every instance of brace-expansion in the dependency tree, regardless of how deeply nested, uses a patched version. This is crucial because a single vulnerable instance anywhere in the tree could be exploited if it processes untrusted input.
Security Improvement
After the upgrade, the same malicious pattern that previously caused memory exhaustion now fails safely:
// With patched version
const pattern = '{' + 'a,'.repeat(10000) + 'b}';
// Parser detects excessive complexity
// Throws error or returns empty result
// Memory usage remains bounded
The patched versions add defensive checks like:
// Conceptual representation of the fix
function parsePattern(pattern) {
const MAX_INTERMEDIATE_SIZE = 10000;
const MAX_NESTING_DEPTH = 10;
if (pattern.length > MAX_PATTERN_LENGTH) {
throw new Error('Pattern too long');
}
// Check complexity before processing
const nestingDepth = countNestingDepth(pattern);
if (nestingDepth > MAX_NESTING_DEPTH) {
throw new Error('Pattern too complex');
}
// Enforce limits during intermediate array creation
let intermediateArray = [];
for (let token of tokens) {
if (intermediateArray.length > MAX_INTERMEDIATE_SIZE) {
throw new Error('Intermediate expansion too large');
}
intermediateArray.push(processToken(token));
}
return intermediateArray;
}
Prevention & Best Practices
1. Implement Defense-in-Depth for User Input
Never trust user-provided patterns or glob expressions without validation:
// Good: Validate before processing
function validatePattern(pattern) {
const MAX_LENGTH = 1000;
const MAX_BRACES = 5;
if (pattern.length > MAX_LENGTH) {
throw new Error('Pattern too long');
}
const braceCount = (pattern.match(/{/g) || []).length;
if (braceCount > MAX_BRACES) {
throw new Error('Pattern too complex');
}
return pattern;
}
app.post('/api/search', (req, res) => {
try {
const pattern = validatePattern(req.body.pattern);
const matcher = new Minimatch(pattern);
// ... proceed safely
} catch (err) {
res.status(400).json({ error: err.message });
}
});
2. Set Resource Limits
Use Node.js resource limits to contain damage from resource exhaustion:
// Set memory and CPU limits
const { Worker } = require('worker_threads');
function processPatternSafely(pattern) {
return new Promise((resolve, reject) => {
const worker = new Worker('./pattern-processor.js', {
resourceLimits: {
maxOldGenerationSizeMb: 100,
maxYoungGenerationSizeMb: 50
}
});
worker.postMessage(pattern);
const timeout = setTimeout(() => {
worker.terminate();
reject(new Error('Pattern processing timeout'));
}, 5000);
worker.on('message', (result) => {
clearTimeout(timeout);
resolve(result);
});
});
}
3. Keep Dependencies Updated
Automate dependency updates with tools that understand security context:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
# Prioritize security updates
open-pull-requests-limit: 10
labels:
- "security"
- "dependencies"
4. Use Allowlists Instead of Blocklists
Instead of trying to detect malicious patterns, define what's acceptable:
// Good: Allowlist approach
const ALLOWED_PATTERN = /^[a-zA-Z0-9_\-*.\/]+$/;
function sanitizePattern(pattern) {
if (!ALLOWED_PATTERN.test(pattern)) {
throw new Error('Pattern contains disallowed characters');
}
// Additional checks for specific constructs
if (pattern.includes('..')) {
throw new Error('Range expansion not allowed');
}
return pattern;
}
5. Monitor Resource Usage
Implement monitoring to detect DoS attacks in progress:
const v8 = require('v8');
function checkMemoryUsage() {
const heapStats = v8.getHeapStatistics();
const usedPercent = (heapStats.used_heap_size / heapStats.heap_size_limit) * 100;
if (usedPercent > 90) {
console.error('Memory usage critical:', usedPercent.toFixed(2), '%');
// Trigger alerts, reject new requests, etc.
}
}
// Check every 10 seconds
setInterval(checkMemoryUsage, 10000);
Security Standards Reference
This vulnerability maps to several security standards:
- CWE-400: Uncontrolled Resource Consumption ('Resource Exhaustion')
- CWE-1333: Inefficient Regular Expression Complexity (related pattern)
- OWASP Top 10 2021 - A05:2021: Security Misconfiguration (outdated dependencies)
Key Takeaways
- CVE-2026-69152 bypassed the previous CVE-2026-14257 fix by exploiting unbounded intermediate arrays in the parsing phase, demonstrating that vulnerability patches must address all code paths, not just the initially discovered attack vector
- The pnpm-lock.yaml required four separate brace-expansion overrides (1.1.18, 2.1.4, 3.0.6, 5.0.9) to patch all instances across minimatch versions 3, 5, 9, and 10 in the dependency tree
- Dependency tree complexity amplifies risk: A single vulnerable library can appear multiple times at different versions, requiring comprehensive override strategies rather than simple version bumps
- DoS vulnerabilities in parsing libraries are especially dangerous because they're triggered during input processing before application-level validation can occur, making early bounds checking in the library itself essential
- Pattern expansion operations with user input require strict validation: Always limit pattern length, nesting depth, and expansion size before passing to libraries like brace-expansion or minimatch
How Orbis AppSec Detected This
- Source: The vulnerability exists in the brace-expansion library versions 1.1.13, 2.0.3, and 5.0.4 as declared in
pnpm-lock.yaml - Sink: Pattern parsing operations in brace-expansion that create unbounded intermediate arrays when processing nested braces and ranges
- Missing control: Insufficient bounds checking on intermediate data structures during the parsing phase, allowing memory exhaustion before expansion limits apply
- CWE: CWE-400 - Uncontrolled Resource Consumption ('Resource Exhaustion')
- Fix: Upgraded brace-expansion to patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) using pnpm overrides to enforce the fix across all minimatch dependencies in the tree
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-69152 serves as a critical reminder that security vulnerabilities often evolve—attackers find new ways to exploit the same underlying weaknesses even after initial patches. The brace-expansion DoS vulnerability bypassed previous mitigations by targeting intermediate array generation, requiring a comprehensive fix across multiple major versions in the dependency tree.
The fix demonstrates the importance of holistic dependency management: using pnpm overrides to enforce patched versions across the entire dependency tree, regardless of nesting depth. For developers, this case highlights the need for defense-in-depth: validate user input, set resource limits, monitor consumption, and keep dependencies updated with automated security scanning.
By understanding how this specific vulnerability exploited unbounded intermediate arrays during pattern parsing, developers can better recognize similar risks in other parsing libraries and implement appropriate safeguards before vulnerabilities are discovered.