How Denial of Service via Exponential Regex Complexity Happens in Node.js and How to Fix It
The Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Denial of Service via Exponential-Time Brace Expansion |
| CWE | CWE-1333 — Inefficient Regular Expression Complexity |
| Language | JavaScript / Node.js |
| Risk | Remote attacker can hang or crash a web service |
| Root Cause | O(2ⁿ) worst-case expansion complexity in brace-expansion |
| Fix | Upgrade to brace-expansion 1.1.16 / 2.1.2 / 5.0.7 |
Introduction
This high-severity Denial of Service vulnerability could have allowed any remote attacker to monopolize CPU threads in a production web service — with nothing more than a single HTTP request containing a cleverly crafted string. The culprit is brace-expansion, a ubiquitous Node.js utility that expands shell-style brace patterns like {a,b,c} into arrays of strings. It sits deep in the dependency trees of tools like glob, minimatch, and many others, making it nearly invisible — yet its algorithmic flaw, tracked as CVE-2026-13149, is directly reachable in any application that processes user-influenced file paths, glob patterns, or search strings.
The vulnerable package was present in package-lock.json at version 2.1.1. Because this is a web service where request handlers process user-influenced input, Trivy flagged the pattern as likely exploitable in production.
The Vulnerability Explained
What Does brace-expansion Do?
brace-expansion takes a string like file.{js,ts,jsx,tsx} and returns ['file.js', 'file.ts', 'file.jsx', 'file.tsx']. It is used heavily by glob-matching libraries to enumerate possible file paths. The library is downloaded hundreds of millions of times per week on npm.
The Algorithmic Time Bomb
The vulnerability lies in how brace-expansion handles nested or repeated brace groups. Consider a pattern like:
{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}
Each {a,b} doubles the number of combinations. With 10 groups, the library must generate 2¹⁰ = 1,024 strings. With 30 groups, that is over 1 billion strings. The expansion is computed eagerly and synchronously, blocking Node.js's single-threaded event loop entirely.
The vulnerable code path in versions prior to the fix performs a Cartesian-product expansion without any upper bound on output size or time spent. This is a classic CWE-1333 pattern: a function whose execution time grows exponentially with input length.
Pre-Fix State in package-lock.json
Before the fix, package-lock.json resolved brace-expansion to version 2.1.1 through transitive dependencies (notably glob and minimatch). There was no explicit top-level pin, meaning the vulnerable version was silently inherited:
// Before fix — no explicit brace-expansion entry in top-level dependencies
"dependencies": {
"archiver": "^8.0.0",
"bcryptjs": "^3.0.3",
"bullmq": "^5.78.1",
...
}
Because brace-expansion@2.1.1 was only a transitive dependency, it was easy to overlook in manual reviews.
Concrete Attack Scenario
This application is a web service. Suppose it exposes an endpoint that accepts a file glob pattern for searching or listing resources — for example:
GET /api/files?pattern={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}
If the server passes req.query.pattern into any function backed by glob or minimatch (which internally calls brace-expansion), the library will attempt to expand 8⁴ = 4,096 strings synchronously. Scale the exponent up slightly and the event loop is blocked for seconds. Send a handful of concurrent requests and the service is effectively down — no authentication required, no special privileges, just an HTTP GET.
Even without a direct glob endpoint, brace-expansion may be invoked indirectly through build tooling, file-watching middleware, or template engines that process user input.
The Fix
What Changed
The fix makes two targeted changes to package.json and package-lock.json:
1. Explicit top-level dependency pin in package.json:
"dependencies": {
"archiver": "^8.0.0",
"bcryptjs": "^3.0.3",
+ "brace-expansion": "^2.1.2",
"bullmq": "^5.78.1",
...
}
By adding brace-expansion as a direct dependency, npm is forced to resolve it to ^2.1.2 (the patched version) across the entire dependency tree, overriding any transitive request for 2.1.1.
2. Lock file updated to patched versions:
The package-lock.json now resolves brace-expansion to 2.1.2 (and related packages to their updated equivalents). The lock file diff also updates several @emnapi/* packages that were co-resolved during the upgrade:
-"version": "1.2.1",
-"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
-"integrity": "sha512-uTII7OYF+...",
+"version": "1.2.3",
+"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz",
+"integrity": "sha512-ELEBe8PsL...",
Why Pinning as a Direct Dependency Matters
npm's dependency resolution algorithm will use the highest satisfying version it finds when multiple packages request the same transitive dependency. However, without an explicit top-level pin, a future npm install could still pull in a vulnerable version if a transitive dependency loosens its own version range. Pinning brace-expansion at the top level guarantees the patched version wins every resolution contest.
What the Patched Versions Fix
Versions 1.1.16, 2.1.2, and 5.0.7 of brace-expansion introduce a complexity guard that limits the number of expansions the algorithm will attempt before throwing an error or returning a safe fallback. This changes the worst-case behavior from O(2ⁿ) unbounded CPU consumption to a fast, bounded failure — exactly the right trade-off for a security fix.
Prevention & Best Practices
1. Audit Transitive Dependencies Regularly
Transitive dependencies are the silent majority of your attack surface. Run npm audit and integrate a dedicated SCA scanner (Trivy, Snyk, Socket.dev) into your CI pipeline so that CVEs in indirect dependencies are caught before they reach production.
# Quick check
npm audit --audit-level=high
# Trivy lock-file scan
trivy fs --scanners vuln package-lock.json
2. Validate and Limit Pattern Inputs
If your application accepts glob patterns or file path expressions from users, impose strict length limits and character allowlists before passing them to any expansion library:
// Example guard before using glob
const MAX_PATTERN_LENGTH = 256;
const SAFE_PATTERN = /^[a-zA-Z0-9_\-\/\.\*\?\[\]{}]+$/;
function safeGlob(pattern) {
if (typeof pattern !== 'string' || pattern.length > MAX_PATTERN_LENGTH) {
throw new Error('Invalid pattern');
}
if (!SAFE_PATTERN.test(pattern)) {
throw new Error('Pattern contains unsafe characters');
}
return glob.sync(pattern);
}
3. Pin Critical Security Dependencies Explicitly
For packages with a history of algorithmic complexity issues (regex engines, parsers, expanders), add them as explicit top-level dependencies in package.json even if you don't use them directly. This gives you control over the resolved version.
4. Use Lockfile Integrity Checks in CI
Commit package-lock.json to version control and use npm ci (not npm install) in CI/CD pipelines. npm ci enforces the exact lock file, preventing silent version drift.
# In CI — always use npm ci
npm ci --ignore-scripts
5. Reference Security Standards
- CWE-1333: Inefficient Regular Expression Complexity
- CWE-400: Uncontrolled Resource Consumption
- OWASP: Denial of Service Cheat Sheet
Key Takeaways
- Transitive dependencies are in scope for attackers.
brace-expansionwas never imported directly in application code, yet it was reachable throughglob/minimatchand exploitable via user-supplied HTTP request parameters. - Exponential-time algorithms are DoS vulnerabilities. Any function that performs Cartesian-product expansion, recursive backtracking, or unbounded iteration on user-controlled input is a potential availability risk, not just a performance concern.
- Pinning
brace-expansionexplicitly inpackage.jsonis the correct fix. Simply upgrading a transitive dependency in the lock file is fragile; adding it as a direct dependency with^2.1.2ensures the patched version wins all future resolution conflicts. - A single HTTP request is sufficient to exploit this. No authentication, no special permissions — just a crafted pattern string sent to any endpoint that triggers glob matching.
- Automated scanning of
package-lock.jsoncatches what code review misses. Manual review of application source code would never surface this vulnerability; only SCA tooling scanning the lock file found it.
How Orbis AppSec Detected This
- Source: User-influenced input (e.g., HTTP request query parameters or body fields) passed as glob pattern strings into libraries backed by
brace-expansion. - Sink: The
brace-expansionpackage's internal expansion function, invoked transitively throughglob/minimatchdependencies listed inpackage-lock.json. - Missing control: No upper bound on expansion complexity; no explicit version pin to force resolution to a patched release.
- CWE: CWE-1333 — Inefficient Regular Expression Complexity (also CWE-400 — Uncontrolled Resource Consumption).
- Fix: Added
"brace-expansion": "^2.1.2"as an explicit top-level dependency inpackage.jsonand regeneratedpackage-lock.jsonto resolve all instances to the 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-13149 is a reminder that the most dangerous vulnerabilities in modern Node.js applications often hide not in the code you write, but in the packages your packages depend on. A single transitive dependency — brace-expansion at version 2.1.1 — carried an exponential-time complexity flaw that could bring down an entire web service with a single malformed HTTP request. The fix is precise and non-breaking: upgrade to 2.1.2 and pin it explicitly so the resolution is stable across future installs.
Treat your lock file as a security artifact. Scan it, pin critical packages, and automate the detection of CVEs before they reach production.