How Denial-of-Service via Unbounded Array Expansion Happens in JavaScript and How to Fix It
The Problem with "Fixed" Vulnerabilities
Security patches are not always final. Sometimes a fix closes one door while leaving a window cracked open. That is exactly what happened with brace-expansion, a widely-used Node.js utility that converts shell-style brace patterns like {a,b,c} into expanded string arrays. A previous vulnerability—CVE-2026-14257—was patched by adding a check on the output array length. CVE-2026-69152 bypasses that check entirely by attacking the intermediate arrays created during expansion, before any length guard is ever consulted.
This post explains how the bypass works, what was changed to fix it, and how to make sure your own projects are not quietly running a vulnerable version.
The Vulnerability Explained
What brace-expansion Does
brace-expansion is a dependency pulled in by hundreds of popular packages—glob, minimatch, fast-glob, micromatch, and many others. It takes a pattern string and expands it:
const expand = require('brace-expansion');
expand('{a,b}{1,2,3}');
// => ['a1', 'a2', 'a3', 'b1', 'b2', 'b3']
This is used everywhere: build tools, test runners, file watchers, CLI utilities. If any of those tools accept user-supplied glob patterns—even indirectly—the expansion logic is reachable from attacker-controlled input.
The Original Mitigation (CVE-2026-14257)
The patch for CVE-2026-14257 added a guard roughly equivalent to:
if (expansions.length > MAX_LENGTH) {
throw new RangeError('Brace expansion too large');
}
This check fires when the final expanded array grows beyond a threshold. Reasonable in theory—but the expansion algorithm builds intermediate arrays for each nested brace group before assembling the final result.
How CVE-2026-69152 Bypasses the Mitigation
Consider a crafted pattern like:
{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}{0..9}
Each {0..9} segment generates a 10-element intermediate array. When the algorithm combines them via a Cartesian product, the intermediate result after the first two groups is 100 elements, after three groups 1,000 elements, and so on—reaching 100,000,000 elements after eight groups. The final-output length check is only applied after all this intermediate memory has already been allocated.
An attacker who can supply any string that reaches brace-expansion's expand() function can trigger this growth curve. The process heap fills up, Node.js throws an out-of-memory error (or simply hangs while the garbage collector thrashes), and the application becomes unavailable—a classic ReDoS-style resource exhaustion attack, but targeting memory allocation rather than regex backtracking.
Real-World Reachability in This Repository
The scanner flagged brace-expansion in pnpm-lock.yaml at version 1.1.16. In the dependency tree, brace-expansion is consumed by tooling that processes file globs. If any part of the build pipeline, dev server, or test harness accepts externally-influenced path patterns (e.g., from environment variables, config files committed by contributors, or CLI arguments in CI), the vulnerable expansion path is reachable. Even in a pure build-tool context, a malicious contributor or a compromised upstream package could trigger the DoS during CI, blocking deployments.
The Fix
What Changed
The fix has two parts:
1. Upgrading brace-expansion across all affected version lines
The PR upgrades to the following patched releases:
- 1.x → 1.1.18
- 2.x → 2.1.4
- 3.x → 3.0.6
- 5.x → 5.0.9
Each of these releases adds bounds checking on the intermediate arrays produced during Cartesian product assembly, not just the final output. This closes the bypass that CVE-2026-69152 exploits.
2. Pinning the safe version in package.json
The diff adds an explicit override in package.json:
- "serialize-javascript": "7.0.5"
+ "serialize-javascript": "7.0.5",
+ "brace-expansion": "1.1.18"
This pnpm override (under the pnpm.overrides or resolutions field) forces every transitive dependency that pulls in brace-expansion to resolve to 1.1.18 or higher, regardless of what version range they declare. Without this pin, a future pnpm install could silently re-introduce a vulnerable version if a transitive dependency specifies a range that resolves to an older release.
Before and After
Before — pnpm-lock.yaml resolved brace-expansion to 1.1.16:
brace-expansion@1.1.16:
resolution: {integrity: sha512-...}
After — resolves to 1.1.18:
brace-expansion@1.1.18:
resolution: {integrity: sha512-...}
The patched version introduces intermediate-array size tracking. Internally, the expansion loop now checks the running size of the Cartesian product before allocating the next batch of intermediate strings, throwing a RangeError early if the expansion would exceed a safe threshold—rather than waiting until the final array is assembled.
Why Both Files Matter
pnpm-lock.yamlrecords the exact resolved version installed on disk. Updating it ensures the current install is safe.package.jsonrecords the intent for future installs. Without the explicit pin there,pnpm installafter a lockfile reset could pull a vulnerable version again from a transitive dependency's loose semver range.
Prevention & Best Practices
1. Use Dependency Overrides / Resolutions for Transitive Vulnerabilities
When a vulnerability lives in a transitive dependency you don't control directly, use your package manager's override mechanism:
// package.json (pnpm)
{
"pnpm": {
"overrides": {
"brace-expansion": ">=1.1.18"
}
}
}
// package.json (yarn)
{
"resolutions": {
"brace-expansion": "1.1.18"
}
}
2. Validate User-Supplied Glob Patterns
If your application accepts glob or brace-expansion patterns from users, apply a length and complexity check before passing them to any expansion library:
const MAX_PATTERN_LENGTH = 256;
function safeExpand(pattern) {
if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) {
throw new Error('Pattern too long or invalid');
}
return braceExpansion(pattern);
}
This defense-in-depth measure limits blast radius even if a future bypass is discovered.
3. Run SCA Scans in CI
Integrate a Software Composition Analysis tool (Trivy, Snyk, npm audit, or OWASP Dependency-Check) into your CI pipeline so vulnerable transitive dependencies are caught before they reach production:
# GitHub Actions example
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
exit-code: '1'
4. Watch for "Bypass" CVEs on Previously Patched Packages
CVE-2026-69152 is a textbook example of a mitigation bypass. When a package receives a CVE patch, monitor its advisory feed for follow-on CVEs that describe bypasses of the original fix. Subscribe to GitHub Security Advisories for packages you depend on.
5. Relevant Standards
- CWE-400: Uncontrolled Resource Consumption — the root CWE for this vulnerability class.
- OWASP A06:2021 – Vulnerable and Outdated Components: Keeping dependencies current and scanning for known CVEs is a core OWASP Top 10 control.
Key Takeaways
- CVE-2026-69152 is a bypass, not a new bug class: The intermediate-array growth vector was always present; the CVE-2026-14257 patch simply didn't cover it. Never assume a single patch fully closes a resource-exhaustion vector.
- Pinning in
package.jsonis as important as updatingpnpm-lock.yaml: The lockfile records today's state; the override inpackage.jsonprotects future installs from regressing. brace-expansionis a deeply transitive dependency: It appears in the dependency trees ofglob,minimatch,fast-glob, and many CLI tools. Any Node.js project using file globbing is likely affected.- Intermediate-array bounds checking is the correct fix: The patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) add size checks during Cartesian product assembly, not only at the end—this is the architectural change that closes the bypass.
- Defense-in-depth matters: Even with the patched library, validating the length and structure of user-supplied glob patterns before expansion reduces exposure to any future bypasses.
How Orbis AppSec Detected This
- Source: User-influenced or contributor-supplied brace-expansion pattern strings reaching the
expand()function via glob-processing toolchain dependencies recorded inpnpm-lock.yaml. - Sink: The
brace-expansionpackage's internal Cartesian product loop, which allocates intermediate arrays without an upper-bound check in versions prior to 1.1.18 / 2.1.4 / 3.0.6 / 5.0.9. - Missing control: No intermediate-array size limit in the expansion algorithm; the only existing guard (added for CVE-2026-14257) checked the final output array length, which is reached too late to prevent memory exhaustion.
- CWE: CWE-400 – Uncontrolled Resource Consumption.
- Fix: Upgraded
brace-expansionto1.1.18inpnpm-lock.yamland added an explicit version pin inpackage.jsonto prevent transitive dependency resolution from regressing to a vulnerable release.
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 is a sharp reminder that security patches have a shelf life and that mitigation bypasses are a real threat model. The brace-expansion library's original CVE-2026-14257 fix was a reasonable first step, but it left an exploitable gap in the intermediate expansion phase. The patched versions (1.1.18, 2.1.4, 3.0.6, 5.0.9) close that gap by adding bounds checks where the memory actually gets allocated.
For developers, the lesson is practical: keep SCA scanning in your CI pipeline, use package manager overrides to enforce safe versions across your entire dependency tree, and treat "bypass CVE" advisories with the same urgency as original findings. A vulnerability that was "already patched" is not necessarily safe.