How Denial of Service via Unbounded Intermediate Arrays Happens in JavaScript and How to Fix It
Introduction
The frontend/package-lock.json file governs every transitive JavaScript dependency the frontend build installs — including low-level utility libraries that most developers never think about. One of those utilities, brace-expansion, is responsible for expanding shell-style brace patterns like {a,b,c} or {1..100} into flat string arrays. It is pulled in automatically by minimatch, which is itself a dependency of many popular build tools.
In this project, brace-expansion was pinned at version 1.1.14. That version contains a high-severity Denial of Service vulnerability tracked as CVE-2026-69152: a carefully crafted input pattern causes the library to build intermediate arrays with no upper bound on their size, consuming all available heap memory and crashing the Node.js process. What makes this CVE particularly interesting is that it is not a brand-new class of bug — it is a bypass of the mitigation that was already shipped for the earlier CVE-2026-14257. The attacker community found a way around the guard rail, and 1.1.14 never received the follow-up patch.
The Vulnerability Explained
What brace-expansion does
brace-expansion takes a string such as "file.{js,ts,tsx}" and returns ["file.js", "file.ts", "file.tsx"]. For numeric ranges like "{1..10000}" it generates every integer in that range. Internally the library builds intermediate arrays at each expansion step before concatenating them into the final result.
Where 1.1.14 falls short
The CVE-2026-14257 patch added a check to limit the total number of final expanded strings. However, the intermediate arrays assembled during the recursive expansion steps were not subject to the same limit. An attacker can craft a nested pattern where the intermediate arrays grow exponentially before the final-count guard is ever evaluated:
# Example of a pathological pattern (illustrative)
"{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}{a,b,c,d,e,f,g,h}...{...}"
Each nested brace group multiplies the size of the intermediate result array. Because 1.1.14's guard only inspects the output length, not the working memory during expansion, the process can allocate gigabytes of heap before the check fires — or before the check fires at all if the pattern is designed to stay just under the output threshold while maximising intermediate allocations.
The vulnerable dependency declaration in package-lock.json
Before the fix, the node_modules/brace-expansion block read:
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
And the legacy "dependencies" section of the lockfile mirrored the same version. Meanwhile, minimatch declared a loose range:
"minimatch": {
"requires": {
"brace-expansion": "^1.1.7"
}
}
The ^1.1.7 range permits any 1.x release ≥ 1.1.7, but npm install had resolved it to 1.1.14 and frozen that in the lockfile — meaning the vulnerable version was locked in place and would not auto-upgrade.
Real-world attack scenario
If any part of this frontend's build pipeline or server-side rendering layer passes user-controlled strings to a function that internally calls minimatch or brace-expansion (e.g., a file-glob API, a template engine, or a search filter that supports glob syntax), an attacker can send a POST body containing a malicious pattern. The Node.js process expands the pattern, intermediate arrays balloon unchecked, the heap limit is hit, and the process crashes or becomes unresponsive — a classic CWE-400 Uncontrolled Resource Consumption scenario.
Even if the application does not expose glob inputs directly today, the vulnerability lives in the dependency tree and can be triggered by future feature additions or by tooling that runs in CI/CD pipelines.
The Fix
Upgrading brace-expansion to 1.1.18
The pull request makes two targeted changes to frontend/package-lock.json (and a corresponding update to frontend/package.json).
Change 1 — the node_modules/brace-expansion block:
"node_modules/brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
+ "license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
Change 2 — the legacy "dependencies" section:
"brace-expansion": {
- "version": "1.1.14",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
- "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
+ "version": "1.1.18",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
+ "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
Change 3 — pinning minimatch's transitive requirement:
"minimatch": {
"requires": {
- "brace-expansion": "^1.1.7"
+ "brace-expansion": "1.1.18"
}
}
Why each change matters
| Change | Purpose |
|---|---|
Bump node_modules/brace-expansion to 1.1.18 |
Ensures npm ci installs the patched version in the module tree |
Bump the legacy lockfile brace-expansion entry |
Keeps both lockfile formats consistent; older npm clients read the flat section |
Pin minimatch → brace-expansion to exact 1.1.18 |
Prevents a future npm install from resolving the loose ^1.1.7 range back to a vulnerable version |
Version 1.1.18 introduces bounds checking on the intermediate arrays during recursive expansion, not just on the final output count. This closes the bypass that made CVE-2026-14257's mitigation ineffective.
Prevention & Best Practices
1. Treat lockfiles as security artifacts
package-lock.json is not just a performance optimisation — it is the authoritative record of every dependency version your application will install. Commit it, review it in PRs, and never .gitignore it.
2. Run a vulnerability scanner in CI
Trivy (used here), npm audit, Snyk, and Socket.dev all maintain databases of known-vulnerable package versions. Add one as a required CI step so that new CVEs are caught before they reach production.
# Minimal CI gate using npm audit
npm audit --audit-level=high
3. Use exact version pins for security-sensitive transitive deps
The minimatch fix demonstrates a useful technique: when a transitive dependency has a known-vulnerable range, override it with an exact version pin inside the lockfile. This prevents accidental regression during routine npm install runs.
4. Enable Dependabot or Renovate
Automated dependency update bots create PRs when new versions are published, keeping the gap between a CVE disclosure and your upgrade as small as possible.
5. Limit glob/pattern inputs from users
If your application passes any user-controlled string to a glob-matching function, validate or sanitise the input first:
// Reject patterns that could cause exponential expansion
function isSafeGlobPattern(pattern) {
// Limit total length
if (pattern.length > 256) return false;
// Limit nesting depth of braces
const braceDepth = (pattern.match(/\{/g) || []).length;
if (braceDepth > 3) return false;
return true;
}
Relevant standards
- CWE-400: Uncontrolled Resource Consumption — https://cwe.mitre.org/data/definitions/400.html
- OWASP — Denial of Service: https://owasp.org/www-community/attacks/Denial_of_Service
Key Takeaways
- The CVE-2026-14257 mitigation in brace-expansion 1.1.14 was incomplete: it guarded final output size but left intermediate array allocation unbounded, creating a bypass that CVE-2026-69152 exploits.
- Lockfile pinning matters: the loose
"brace-expansion": "^1.1.7"range inminimatch'srequiresblock would have allowed npm to resolve back to a vulnerable version; the fix pins it to the exact safe release1.1.18. - Transitive dependencies carry real risk:
brace-expansionis three levels deep in the dependency tree, yet a single malicious string passed tominimatchcan crash the process. - Both lockfile sections must be updated:
package-lock.jsoncontains both anode_modules/tree section and a legacy flat"dependencies"section; updating only one leaves the other out of sync and can cause inconsistent installs across npm versions. - Trivy caught what manual review would likely miss: no developer routinely audits the
integrityhash of a three-level-deep transitive dependency — automated scanning is the practical defence here.
How Orbis AppSec Detected This
- Source: The
brace-expansionpackage version string ("1.1.14") declared infrontend/package-lock.json— data that originates from the npm registry resolution of user-installed dependencies. - Sink: Any call site within the application's dependency tree that invokes
brace-expansion's internal array-building logic with an attacker-controlled pattern string (reachable throughminimatchand any library that depends on it). - Missing control: No upper bound on intermediate array allocation during recursive brace expansion; the existing CVE-2026-14257 guard only checked final output length, not working memory.
- CWE: CWE-400 — Uncontrolled Resource Consumption.
- Fix:
brace-expansionwas upgraded from1.1.14to1.1.18in both sections offrontend/package-lock.json, andminimatch's transitive requirement was pinned to the exact patched version.
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 are not always final. The brace-expansion maintainers shipped a fix for CVE-2026-14257, but an attacker-visible bypass remained in the intermediate array allocation path — and versions that never received the follow-up patch (like 1.1.14) stayed vulnerable. Upgrading to 1.1.18 closes both the original issue and the bypass in one step.
For JavaScript developers, the lesson is clear: keep lockfiles up to date, scan them automatically in CI, and treat transitive dependencies with the same security scrutiny you apply to first-party code. A three-line change to package-lock.json is all it takes to eliminate a high-severity DoS vector.