How Denial of Service via Exponential Complexity Happens in JavaScript and How to Fix It
The Problem Hidden in Your Lock File
The frontend/app/react-native/package-lock.json file in this React Native project contained a single pinned entry that looked completely harmless:
"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==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
That version number — 1.1.14 — is the vulnerability. It corresponds to a build of brace-expansion that contains no guard against exponential-time expansion of nested brace patterns, and it was flagged as CVE-2026-13149 with a HIGH severity rating by the Trivy scanner.
The Vulnerability Explained
What is brace-expansion?
brace-expansion is one of the most widely transitive npm packages in existence. It implements POSIX-style brace expansion — the same feature that lets you type {src,test}/**/*.js in a shell and have it expand to both src/**/*.js and test/**/*.js. Virtually every glob library (glob, minimatch, fast-glob) depends on it, which is why it appears in nearly every JavaScript project's dependency tree.
The exponential complexity trap
The vulnerability lies in how version 1.1.14 handles deeply nested or repeatedly chained brace groups. Consider this pattern:
{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}
This is only 50 characters long. Yet when fed to the vulnerable expand() function in brace-expansion@1.1.14, it must produce 2¹⁰ = 1,024 combinations. Double the groups to 20 and you get 2²⁰ = over one million strings. At 30 groups: over one billion. The expansion time and memory usage grow exponentially with the number of groups, with no internal limit to stop it.
The root cause is that the library's internal expand() function naively enumerates the Cartesian product of all brace alternatives without bounding the total output size. The dependency on concat-map (present in 1.1.14 but removed in 2.1.2) is a clue — concat-map is used to flatten the intermediate expansion arrays, and that flattening step is the source of the unbounded allocation.
Attack scenario specific to this application
This React Native application uses brace-expansion indirectly through its build toolchain (expo, babel-preset-expo, @expo/prebuild-config). Any code path in the build pipeline or at runtime that:
- Accepts a user-supplied file path, glob pattern, or configuration string
- Passes it (directly or via
minimatch/glob) to a function that internally callsbrace-expansion
…is potentially exploitable. An attacker who can control a glob pattern — for example through a configuration endpoint, a file-upload path parameter, or a crafted package.json name field processed during build — could submit a payload like:
{A,B,C,D}{A,B,C,D}{A,B,C,D}{A,B,C,D}{A,B,C,D}{A,B,C,D}{A,B,C,D}{A,B,C,D}
This 8-group, 4-alternative pattern expands to 4⁸ = 65,536 strings and would cause the Node.js process to spike to 100% CPU for a measurable period. Scale it up and the process hangs indefinitely, constituting a full Denial of Service.
The Fix
The pull request makes two targeted changes to frontend/app/react-native/package-lock.json (and the corresponding package.json).
1. Upgrade the top-level brace-expansion entry
Before:
"node_modules/brace-expansion": {
"version": "1.1.14",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP...",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
}
After:
"node_modules/brace-expansion": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz",
"integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==",
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
}
Notice that concat-map has been dropped as a dependency in 2.1.2. This is not cosmetic — it reflects a rewrite of the internal expansion logic that no longer relies on unbounded array concatenation to build the Cartesian product.
2. Remove the scoped override for @expo/prebuild-config
Before, the lock file contained a separate, nested brace-expansion entry specifically for @expo/prebuild-config:
"node_modules/@expo/prebuild-config/node_modules/brace-expansion": {
"version": "2.1.2",
...
"dev": true,
...
}
After, this nested override is removed entirely. Because the top-level node_modules/brace-expansion is now already at 2.1.2, the nested override is redundant. npm's deduplication will resolve both the top-level and @expo/prebuild-config's requirement from the same safe version.
3. Explicit dependency in package.json
"brace-expansion": "^2.1.2"
Adding brace-expansion as an explicit direct dependency in package.json ensures that npm's resolution algorithm will always select at least version 2.1.2, even if a transitive dependency tries to pull in an older version. This is the override-by-declaration pattern — a robust way to force a minimum safe version across the entire dependency graph.
Prevention & Best Practices
1. Use lock files and audit them regularly
package-lock.json pins exact versions of every transitive dependency. Run npm audit (or npx audit-ci) in CI to catch known CVEs before they reach production:
npm audit --audit-level=high
2. Validate and sanitise glob patterns before expansion
If your application accepts user-supplied file paths or glob patterns, enforce limits before passing them to any expansion function:
const MAX_PATTERN_LENGTH = 256;
const MAX_BRACE_DEPTH = 3;
function safeBraceCount(pattern) {
const openBraces = (pattern.match(/\{/g) || []).length;
return openBraces <= MAX_BRACE_DEPTH;
}
if (pattern.length > MAX_PATTERN_LENGTH || !safeBraceCount(pattern)) {
throw new Error('Pattern exceeds safe complexity limits');
}
3. Pin overrides for critical transitive dependencies
npm 8+ supports the overrides field in package.json to force a minimum version across the entire tree:
{
"overrides": {
"brace-expansion": "^2.1.2"
}
}
4. Integrate SCA scanning into CI/CD
Tools like Trivy, Snyk, and Socket can scan package-lock.json on every pull request and block merges that introduce known-vulnerable packages. The Trivy rule CVE-2026-13149 is what caught this issue.
5. Relevant standards
- OWASP A06:2021 – Vulnerable and Outdated Components: Keeping dependencies current is a first-class security control.
- CWE-1333 – Inefficient Regular Expression Complexity: The canonical weakness class for algorithmic complexity attacks.
- CWE-400 – Uncontrolled Resource Consumption: The broader category covering CPU and memory exhaustion.
Key Takeaways
brace-expansion1.1.14 inpackage-lock.jsonis the direct source of the vulnerability — the fix is version-specific, not a code change in application logic.- Removing the
concat-mapdependency in 2.1.2 is architecturally significant — it signals a rewrite of the expansion algorithm, not just a patch on top of vulnerable code. - Adding
brace-expansion: ^2.1.2as an explicit dependency inpackage.jsonis the correct way to prevent npm from silently downgrading back to a vulnerable version during future installs. - Nested lock-file overrides (like the
@expo/prebuild-configscoped entry) become dead weight once the top-level package is upgraded — removing them keeps the lock file clean and avoids confusion. - Transitive DoS vulnerabilities are easy to miss because the vulnerable package is never imported directly by application code; only automated SCA scanning reliably surfaces them.
How Orbis AppSec Detected This
- Source: Any code path that passes a user-influenced or externally sourced string as a glob or file-path pattern to
minimatch,glob, or any library that internally invokesbrace-expansion. - Sink: The
expand()function insidenode_modules/brace-expansion/index.js(version 1.1.14), which performs unbounded Cartesian-product expansion of brace groups. - Missing control: No depth limit, no output-size cap, and no timeout guard on the expansion loop in version 1.1.14.
- CWE: CWE-1333 — Inefficient Regular Expression Complexity (also CWE-400 — Uncontrolled Resource Consumption).
- Fix: The
node_modules/brace-expansionentry infrontend/app/react-native/package-lock.jsonwas upgraded from1.1.14to2.1.2, which replaces the exponential expansion algorithm with a bounded implementation and drops theconcat-mapdependency.
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 Denial of Service risk doesn't always come from your own code. A single pinned version number in a lock file — "version": "1.1.14" — was all it took to expose the entire React Native application to potential CPU exhaustion. The fix is surgical: two lines changed in package-lock.json, one line added to package.json, and the attack surface disappears entirely.
The broader lesson is that transitive dependencies deserve the same scrutiny as first-party code. Automated SCA scanning, combined with explicit dependency overrides and regular npm audit runs in CI, is the practical defence. Version 2.1.2 of brace-expansion removes concat-map, rewrites the expansion algorithm, and closes this vulnerability for good.
References
- CWE-1333: Inefficient Regular Expression Complexity
- CWE-400: Uncontrolled Resource Consumption
- OWASP A06:2021 – Vulnerable and Outdated Components
- OWASP Denial of Service Cheat Sheet
- brace-expansion npm package (official)
- Semgrep rules for dependency vulnerabilities
- fix: upgrade brace-expansion to 5.0.7, 1.1.16, 2.1.2 (CVE-2026-13149)