How Denial of Service via Unbounded Brace Expansion Happens in Node.js and How to Fix It
The Problem Hidden in Your package-lock.json
Most developers never think twice about brace-expansion. It quietly powers glob matching in tools like minimatch, glob, mocha, and nyc—the kind of foundational utility that gets pulled into nearly every Node.js project transitively. But in all versions through 5.0.7, brace-expansion contains a high-severity Denial of Service vulnerability (CVE-2026-14257) that can crash your Node.js process with a single malformed string.
The vulnerability was flagged by Trivy in package-lock.json and fixed by upgrading to brace-expansion 5.0.8 (and corresponding patches for older major versions: 3.0.3, 2.1.3, and 1.1.17). This post explains exactly how the attack works, what changed in the fix, and how to protect your own projects.
The Vulnerability Explained
What Does brace-expansion Actually Do?
brace-expansion parses shell-style brace patterns and expands them into arrays of strings:
const expand = require('brace-expansion');
expand('{a,b,c}'); // → ['a', 'b', 'c']
expand('file{1..5}.txt'); // → ['file1.txt', 'file2.txt', ..., 'file5.txt']
expand('{a,b}{c,d}'); // → ['ac', 'ad', 'bc', 'bd']
This is useful and intentional. The problem arises when the library applies this expansion logic to adversarially crafted input without any upper bound on how large the resulting array can grow.
The Unbounded Expansion Problem
Consider what happens when you nest or repeat brace groups:
// Each level doubles the output
expand('{a,b}'); // 2 strings
expand('{a,b}{a,b}'); // 4 strings
expand('{a,b}{a,b}{a,b}'); // 8 strings
// ...
expand('{a,b}'.repeat(30)); // 2^30 = ~1,073,741,824 strings
A 60-character input string produces over one billion output strings. Each string is allocated on the JavaScript heap. Before the fix, brace-expansion would dutifully attempt to construct this entire array, consuming gigabytes of memory until the Node.js process was killed by the OS or threw a fatal JavaScript heap out of memory error:
FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
1: 0xb7c6e0 node::Abort() [node]
2: 0xa9157e node::FatalError(char const*, char const*) [node]
3: 0xdce59e v8::Utils::ReportOOMFailure(...) [node]
The root cause is CWE-400: Uncontrolled Resource Consumption. The expansion algorithm in versions ≤5.0.7 allocates result arrays proportional to the combinatorial product of all brace groups, with no check on the total output size.
Attack Scenario
Imagine a Node.js application that accepts a glob pattern from an HTTP query parameter to search files:
// A simplified but realistic example of a vulnerable code path
const glob = require('glob');
const app = require('express')();
app.get('/files', (req, res) => {
const pattern = req.query.pattern; // user-controlled input
// glob internally uses brace-expansion to parse the pattern
glob(pattern, (err, files) => {
res.json(files);
});
});
An attacker sends:
GET /files?pattern={a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}
That's a 120-character query string. The server attempts to expand 2³⁰ strings, runs out of memory, and crashes—taking down the entire service. No authentication required. No special privileges. Just one HTTP request.
Even if your application doesn't directly expose glob patterns to users, any code path that passes user-influenced data through a library that depends on brace-expansion (such as minimatch, glob, or mocha's file watcher) is potentially vulnerable.
The Fix
The fix in this PR upgrades brace-expansion across all affected major version lines:
| Major Version | Vulnerable | Patched |
|---|---|---|
| 5.x | ≤ 5.0.7 | 5.0.8 |
| 3.x | ≤ 3.0.2 | 3.0.3 |
| 2.x | ≤ 2.1.2 | 2.1.3 |
| 1.x | ≤ 1.1.16 | 1.1.17 |
What Changed in the Patched Versions
The patched versions introduce an output size limit inside the expansion logic. Instead of blindly building the full combinatorial array, the library now checks whether the projected expansion size exceeds a safe threshold and throws an error (or returns an empty/truncated result) rather than attempting to allocate unbounded memory.
Conceptually, the fix adds a guard like this inside the core expansion loop:
// BEFORE (vulnerable — no size check):
function expand(str) {
// ... parse brace groups ...
let result = [];
for (const combo of combinations) {
result.push(combo); // unbounded — could be billions of entries
}
return result;
}
// AFTER (patched — output size is bounded):
const MAX_EXPANSION = 1_000_000; // or similar safe limit
function expand(str) {
// ... parse brace groups ...
const projectedSize = computeExpansionSize(groups);
if (projectedSize > MAX_EXPANSION) {
throw new RangeError('brace-expansion: expansion too large');
}
let result = [];
for (const combo of combinations) {
result.push(combo);
}
return result;
}
This means that legitimate, bounded patterns—{src,test}/**/*.js, file{1..100}.txt—continue to work exactly as before. Only pathologically large expansions are rejected.
The package-lock.json Change
The PR modifies package-lock.json to pin the resolved version of brace-expansion to the patched release. Here is the relevant portion of the diff showing the version bump pattern (the same change is applied to each occurrence of brace-expansion in the dependency tree):
- "version": "5.0.7",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
The PR also bumps a co-located transitive dependency, js-yaml, from 3.15.0 to 3.15.1:
- "version": "3.15.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
+ "version": "3.15.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
"dev": true,
+ "license": "MIT",
This is a housekeeping update that also adds an explicit license field, consistent with the broader lockfile modernization in this PR.
Prevention & Best Practices
1. Validate User-Supplied Glob Patterns Before Expansion
Even with the patched library, defense-in-depth is valuable. If your application accepts user-supplied patterns, validate them before passing to any glob or path-expansion function:
function isSafeGlobPattern(pattern) {
// Reject patterns with excessive brace groups
const braceGroupCount = (pattern.match(/\{/g) || []).length;
if (braceGroupCount > 10) return false;
if (pattern.length > 256) return false;
return true;
}
app.get('/files', (req, res) => {
const pattern = req.query.pattern;
if (!isSafeGlobPattern(pattern)) {
return res.status(400).json({ error: 'Invalid pattern' });
}
glob(pattern, (err, files) => res.json(files));
});
2. Keep Dependencies Updated with Automated Scanning
Use tools like npm audit, Trivy, Snyk, or Dependabot in your CI pipeline to catch vulnerable transitive dependencies before they reach production:
# Run on every CI build
npm audit --audit-level=high
# Or with Trivy
trivy fs --scanners vuln package-lock.json
3. Use overrides in package.json for Transitive Dependency Pinning
If a transitive dependency is slow to update, npm's overrides field lets you force a specific version across the entire dependency tree:
{
"overrides": {
"brace-expansion": "^5.0.8"
}
}
4. Apply Resource Limits at the Process Level
As a last line of defense, consider running Node.js with explicit heap limits and process isolation so that a single OOM event doesn't take down your entire service:
# Limit heap to 512MB; the process will crash before consuming all system memory
node --max-old-space-size=512 server.js
Pair this with a process manager like PM2 that auto-restarts crashed workers.
Security Standards Reference
- CWE-400: Uncontrolled Resource Consumption — the root cause category for this vulnerability
- OWASP A05:2021 – Security Misconfiguration (using known-vulnerable components)
- OWASP Dependency-Check and npm audit are the recommended tools for detecting this class of issue
Key Takeaways
- A 120-character input string can crash your server: The combinatorial nature of brace expansion means output size grows exponentially with input length—input size limits alone are not sufficient protection.
- Transitive dependencies are attack surface:
brace-expansionis rarely a direct dependency; it arrives viaglob,minimatch,mocha, ornyc. Yourpackage-lock.jsonis the ground truth for what version is actually running. - The fix is purely additive: Patched versions of
brace-expansiononly add an output-size guard. All valid, non-adversarial patterns continue to work identically—there is no behavior change for legitimate use cases. - Multiple major versions needed patching simultaneously: The vulnerability existed in the 1.x, 2.x, 3.x, and 5.x lines, meaning projects on any of these versions needed to upgrade to their respective patch release.
- Static analysis caught what code review would miss: No human reviewer scanning application code would spot a vulnerable version of
brace-expansionburied in a lockfile—automated scanning is essential for this class of vulnerability.
How Orbis AppSec Detected This
- Source: User-influenced input (e.g., HTTP query parameters, file path arguments, CLI arguments) passed to any function that internally invokes
brace-expansion'sexpand()function - Sink: The core expansion loop inside
brace-expansion/index.js(versions ≤5.0.7), which allocates result arrays without bounding the total output size - Missing control: No maximum expansion size check before or during array allocation in the
expand()function - CWE: CWE-400 — Uncontrolled Resource Consumption
- Fix: Upgraded
brace-expansionto 5.0.8 (and 3.0.3 / 2.1.3 / 1.1.17 for older major versions) inpackage-lock.json, which introduces an upper bound on expansion output length
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 even the most innocuous-looking utility packages can harbor high-severity vulnerabilities. brace-expansion is so ubiquitous in the Node.js ecosystem that nearly every project with a package-lock.json has it somewhere in the dependency tree. The vulnerability itself is elegant in its simplicity: a small, valid-looking input triggers exponential memory allocation, crashing the process with no authentication or special access required.
The fix is equally simple—upgrade to the patched version. But finding the vulnerability in the first place requires automated scanning of your full dependency tree, not just your direct dependencies. Make npm audit or an equivalent scanner a mandatory step in your CI pipeline, and consider tools like Orbis AppSec to automatically open fix PRs when new CVEs are published.