Back to Blog
high SEVERITY7 min read

How Denial of Service via Exponential Complexity happens in JavaScript and how to fix it

CVE-2026-13149 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package, where specially crafted brace patterns trigger exponential-time processing that can freeze or crash a Node.js application. The fix upgrades the package from version 1.1.14 to 2.1.2 in the React Native frontend's `package-lock.json`, eliminating the vulnerable code path. Because the affected file is in production code, unpatched applications could be targeted by any attacker able to influence b

O
By Orbis AppSec
Published August 26, 2026Reviewed August 26, 2026

Answer Summary

CVE-2026-13149 is a Denial of Service vulnerability (CWE-1333, Inefficient Regular Expression Complexity) in the `brace-expansion` npm package versions before 2.1.2 / 1.1.16. When the library processes a deeply nested or repeated brace pattern such as `{a,b}{a,b}{a,b}...`, its expansion algorithm runs in exponential time, allowing an attacker to exhaust CPU and halt the process. The fix is to upgrade `brace-expansion` to 2.1.2 (or 1.1.16 for the v1 line) in `package-lock.json`, which replaces the exponential expansion logic with a safe, bounded implementation. Orbis AppSec detected the vulnerable version in `frontend/app/react-native/package-lock.json` and automatically opened a pull request with the corrected dependency.

Vulnerability at a Glance

cweCWE-1333 (Inefficient Regular Expression Complexity)
fixUpgrade brace-expansion to 2.1.2 (v2 line) or 1.1.16 (v1 line), which adds safe expansion bounds
riskAn attacker who can supply brace-pattern strings can stall or crash the application process
languageJavaScript / Node.js
root causebrace-expansion 1.1.14 expands nested brace patterns in exponential time with no depth or output limit
vulnerabilityDenial of Service via exponential brace-expansion complexity

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:

  1. Accepts a user-supplied file path, glob pattern, or configuration string
  2. Passes it (directly or via minimatch / glob) to a function that internally calls brace-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-expansion 1.1.14 in package-lock.json is the direct source of the vulnerability — the fix is version-specific, not a code change in application logic.
  • Removing the concat-map dependency 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.2 as an explicit dependency in package.json is 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-config scoped 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 invokes brace-expansion.
  • Sink: The expand() function inside node_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-expansion entry in frontend/app/react-native/package-lock.json was upgraded from 1.1.14 to 2.1.2, which replaces the exponential expansion algorithm with a bounded implementation and drops the concat-map dependency.

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

Frequently Asked Questions

What is a brace-expansion Denial of Service?

It is an attack where a specially crafted string containing nested or repeated curly-brace patterns (e.g., `{a,b,c}{a,b,c}…`) causes the brace-expansion library to generate an exponentially large number of combinations, consuming all available CPU and memory.

How do you prevent brace-expansion DoS in JavaScript?

Upgrade brace-expansion to ≥2.1.2 or ≥1.1.16, validate and limit the length and nesting depth of any user-supplied glob or brace patterns before passing them to expansion functions, and pin dependency versions in package-lock.json.

What CWE is brace-expansion DoS?

CWE-1333 — Inefficient Regular Expression Complexity (also sometimes categorised under CWE-400, Uncontrolled Resource Consumption).

Is input length validation enough to prevent brace-expansion DoS?

Length limits help but are not sufficient alone because a short string like `{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}{a,b}` (40 chars) can still produce 2^10 = 1,024 combinations. The underlying library must also enforce expansion-output limits.

Can static analysis detect brace-expansion DoS?

Yes — software composition analysis (SCA) tools such as Trivy, Snyk, and Dependabot flag known-vulnerable package versions in lock files. Orbis AppSec used Trivy to identify the vulnerable `brace-expansion` 1.1.14 entry in `package-lock.json`.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #280

Related Articles

high

How Denial of Service via infinite loop happens in Node.js dependencies and how to fix it

A high-severity Denial of Service vulnerability in the nanoid package (CVE-2026-67213) was discovered in the project's dependency tree, where crafted input could trigger an infinite loop during random ID generation. The fix upgrades nanoid from 3.3.17 to 3.3.18 and adds an npm override to ensure all transitive dependencies use the patched version.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A Dependabot configuration in `.github/dependabot.yml` was missing cooldown periods for both its npm and GitHub Actions package ecosystems, meaning newly published — potentially malicious or unstable — package versions could be proposed for adoption immediately after release. Adding a `cooldown` block with `default-days: 7` to each ecosystem entry creates a 7-day buffer, allowing the security community time to identify and flag compromised packages before they reach your codebase.

high

How pnpm Missing Minimum Release Age happens in Node.js workspaces and how to fix it

A missing `minimumReleaseAge` setting in `pnpm-workspace.yaml` left this Node.js workspace vulnerable to immediately installing newly published — potentially malicious — package versions. The fix adds `minimumReleaseAge: 10080` (7 days in minutes) to enforce a quarantine window before any freshly published package can be installed. This single configuration change significantly reduces the risk of supply chain attacks targeting the package publishing pipeline.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A high-severity misconfiguration in `.github/dependabot.yml` left three `package-ecosystem` entries without a cooldown period, meaning Dependabot could immediately propose updates from newly published—potentially malicious—packages. The fix adds a `cooldown` block with `default-days: 7` to each entry, introducing a mandatory waiting period before any newly released package version is surfaced as an update candidate. For a Node.js library whose vulnerabilities ripple downstream to all consumers,

critical

How Unauthenticated Proxy Endpoints Enable DoS Amplification in FastAPI and how to fix it

Public proxy endpoints in `backend/api/proxy.py` had no rate limiting, allowing any attacker to flood the httpx connection pool with unauthenticated requests and amplify denial-of-service attacks against downstream tile and coordinate-conversion services. The fix introduces a per-IP sliding-window rate limiter using environment-configurable thresholds, closing the amplification vector without breaking legitimate usage.

high

How Dependabot Missing Cooldown happens in GitHub Actions and how to fix it

A missing `cooldown` block in `.github/dependabot.yml` meant that Dependabot could immediately propose updates to newly published npm packages — including those that may be malicious, compromised, or unstable. By adding a `cooldown` with `default-days: 7`, the project now waits one week before surfacing new package versions, giving the security community time to detect and flag bad releases before they reach production.