Back to Blog
high SEVERITY7 min read

How Denial of Service via Brace Expansion Happens in JavaScript and How to Fix It

A high-severity denial-of-service vulnerability (CVE-2026-13149) in the `brace-expansion` package was fixed by upgrading `concurrently` from `^9.2.1` to `^9.2.4`, which pulls in `shell-quote 1.9.0` instead of the vulnerable `1.8.3`. The flaw allowed an attacker to craft a specially formed brace-expansion pattern that caused exponential processing time, potentially hanging Node.js processes. Left unpatched, any code path that passed user-influenced strings through `concurrently`'s shell-quoting l

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

Answer Summary

CVE-2026-13149 is a high-severity Denial of Service vulnerability in the `brace-expansion` npm package, classified under CWE-1333 (Inefficient Regular Expression Complexity). In JavaScript/Node.js projects, brace-expansion patterns like `{a,b}{c,d}{e,f}...` are expanded combinatorially, and a maliciously crafted pattern can trigger exponential CPU time. The fix is to upgrade `concurrently` to `^9.2.4`, which transitively replaces `shell-quote 1.8.3` (the version that depends on the vulnerable `brace-expansion`) with `shell-quote 1.9.0`, eliminating the vulnerable code path.

Vulnerability at a Glance

cweCWE-1333 (Inefficient Regular Expression Complexity / Catastrophic Backtracking)
fixUpgrade concurrently to ^9.2.4, which transitively upgrades shell-quote from 1.8.3 to 1.9.0 and resolves the vulnerable brace-expansion dependency
riskCPU exhaustion leading to service unavailability
languageJavaScript / Node.js
root causebrace-expansion parses nested/repeated brace patterns in O(2^n) time, allowing a small malicious string to consume unbounded CPU
vulnerabilityDenial of Service via exponential brace-expansion complexity

How Denial of Service via Brace Expansion Happens in JavaScript and How to Fix It

Introduction

The yarn.lock file in this project quietly carried a ticking clock: concurrently@9.2.1 depended on shell-quote@1.8.3, which in turn depended on a version of brace-expansion vulnerable to CVE-2026-13149. This is the kind of transitive dependency risk that is easy to overlook—brace-expansion is not something most developers consciously reach for, yet it sits deep in the dependency graph of dozens of popular packages.

This post walks through exactly what the vulnerability is, how it could be exploited through the concurrentlyshell-quotebrace-expansion chain, and what the one-line upgrade in package.json actually fixes under the hood.


The Vulnerability Explained

What Is Brace Expansion?

Brace expansion is a shell feature (and a JavaScript library that mimics it) that turns a pattern like {a,b,c} into the list ['a', 'b', 'c'], or file{1..5}.txt into ['file1.txt', 'file2.txt', ..., 'file5.txt']. The brace-expansion npm package implements this for use in glob matching and shell-quoting libraries.

The Exponential Complexity Problem

The vulnerability in CVE-2026-13149 is a classic algorithmic complexity attack. When brace patterns are nested or chained, the number of combinations grows exponentially:

{a,b}           → 2  results
{a,b}{c,d}      → 4  results
{a,b}{c,d}{e,f} → 8  results
{a,b}{c,d}{e,f}{g,h}{i,j}{k,l}{m,n}{o,p}{q,r}{s,t}
                → 2^10 = 1,024 results

A string with 30 such two-option groups would require over one billion expansions. The CPU time required is not bounded by any input-length check in the vulnerable version of brace-expansion—the parser faithfully attempts to enumerate every combination.

The Vulnerable Dependency Chain

Before the fix, yarn.lock contained:

concurrently@^9.2.1:
  version "9.2.1"
  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.2.1.tgz#..."
  integrity sha512-fsfrO0MxV64Znoy8/...
  dependencies:
    chalk "4.1.2"
    rxjs "7.8.2"
    shell-quote "1.8.3"   # ← vulnerable version
    supports-color "8.1.1"
    tree-kill "1.2.2"
    yargs "17.7.2"

shell-quote@1.8.3 uses brace-expansion internally to parse and quote shell command strings. The relevant call path is:

  1. concurrently receives a command string (e.g., from a script runner or CI pipeline configuration).
  2. It passes the string to shell-quote for parsing.
  3. shell-quote invokes brace-expansion to expand any brace patterns in the string.
  4. If the string contains a maliciously crafted brace pattern, brace-expansion enters exponential-time processing.

Attack Scenario

Imagine a build tool or CI system that uses concurrently to run multiple commands, where part of the command string is derived from user input (e.g., a branch name, a file path, or a script argument passed via a web UI):

// Simplified example of how concurrently consumes shell-quoted input
const { spawn } = require('concurrently');
spawn([
  { command: userProvidedCommand, name: 'task' }
]);

An attacker who can influence userProvidedCommand could submit:

{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}

This 30-group pattern would require 2^30 (~1.07 billion) expansions. The Node.js process would become unresponsive, effectively achieving denial of service with a single HTTP request or API call.

Real-World Impact for This Project

Even if direct user input does not flow into concurrently in this specific project today, the vulnerable package is present in the dependency tree and could be exercised by:

  • CI/CD pipelines that accept branch names or PR titles as part of command construction.
  • Developer tooling that proxies user-provided arguments to concurrently-backed scripts.
  • Future code changes that inadvertently introduce a user-controlled string into the command path.

The scanner (Trivy) correctly flagged this as "present in dependency tree, not confirmed reachable"—but the fix is cheap and the risk is real enough to warrant immediate remediation.


The Fix

What Changed

The fix required modifications to two files: package.json and yarn.lock.

package.json — Before:

"concurrently": "^9.2.1",

package.json — After:

"concurrently": "^9.2.4",

This version bump is the root cause of all subsequent changes. concurrently@9.2.4 ships with an updated dependency on shell-quote@1.9.0 instead of 1.8.3.

yarn.lock — Before:

concurrently@^9.2.1:
  version "9.2.1"
  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.2.1.tgz#248ea21b95754947be2dad9c3e4b60f18ca4e44f"
  integrity sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==
  dependencies:
    ...
    shell-quote "1.8.3"   # ← vulnerable

yarn.lock — After:

concurrently@^9.2.4:
  version "9.2.4"
  resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-9.2.4.tgz#4cd9bba735ade2ccb287f000f8706513d2f581f8"
  integrity sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==
  dependencies:
    ...
    shell-quote "1.9.0"   # ← patched

And the shell-quote entry itself was updated:

Before:

shell-quote@1.8.3, shell-quote@^1.8.3:
  version "1.8.3"
  ...

After:

shell-quote@1.9.0:
  version "1.9.0"
  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.9.0.tgz#e108b1a136586d5964edb3300016d4bedba0fe57"
  integrity sha512-...

Why Each Change Was Necessary

  • package.json: The semver constraint ^9.2.1 technically allows 9.2.4, but Yarn's lockfile pins exact versions. Changing the constraint to ^9.2.4 forces Yarn to resolve to the new version and regenerate the lockfile entry.
  • yarn.lock: The lockfile is the ground truth for installed versions. Without updating it, yarn install would continue installing concurrently@9.2.1 and shell-quote@1.8.3 regardless of what package.json says, because Yarn prioritizes the lockfile.

How the Fix Solves the Problem

shell-quote@1.9.0 depends on a patched version of brace-expansion that introduces complexity bounds on pattern expansion. The patched library detects when a brace pattern would generate an unreasonably large number of combinations and either caps the expansion or throws a controlled error—preventing the exponential blowup entirely.


Prevention & Best Practices

1. Audit Transitive Dependencies Regularly

The vulnerability was not in a direct dependency (concurrently) but in a transitive one (brace-expansion via shell-quote). Tools that only check package.json would miss this. Use:

npm audit
yarn audit
npx snyk test
trivy fs --security-checks vuln .

2. Lock Files Are Security Artifacts

yarn.lock and package-lock.json are not just reproducibility tools—they are security documents. Treat changes to them with the same scrutiny as application code changes. Review lockfile diffs in pull requests.

3. Validate and Bound User-Supplied Strings Before Shell Operations

If your application passes any user-influenced data through shell-quoting or glob-expansion libraries, add an explicit length and character-set check before the call:

function safeCommand(userInput) {
  // Reject strings that are too long or contain suspicious brace patterns
  if (userInput.length > 512) {
    throw new Error('Command string too long');
  }
  if (/(\{[^}]*\}){5,}/.test(userInput)) {
    throw new Error('Suspicious brace pattern detected');
  }
  return shellQuote.quote([userInput]);
}

4. Enable Automated Dependency Update PRs

Tools like Dependabot, Renovate, or Orbis AppSec can automatically open PRs when vulnerable dependency versions are detected, ensuring fixes are applied quickly without manual monitoring.

5. Security Standards Reference

  • CWE-1333: Inefficient Regular Expression Complexity — the root cause category for algorithmic complexity attacks.
  • CWE-400: Uncontrolled Resource Consumption — the broader category covering DoS via resource exhaustion.
  • OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of why keeping dependencies current is a security requirement, not just a maintenance chore.

Key Takeaways

  • Transitive dependencies carry real risk: The vulnerability was two levels deep (concurrentlyshell-quotebrace-expansion), but it was still reachable and exploitable.
  • yarn.lock must be updated alongside package.json: Changing only the semver constraint in package.json would not have fixed the issue—the lockfile pins the exact installed version.
  • Exponential-time algorithms are DoS vectors: A 30-group brace pattern is a short string (60 characters) that triggers over a billion operations. Input length alone is not a sufficient defense.
  • shell-quote@1.8.3 is the specific version to remove: Any project pinning this version in its lockfile is exposed, even if concurrently is not the entry point.
  • Upgrading concurrently to ^9.2.4 is the minimal, safe fix: The change is scoped to dependency resolution and does not alter any application logic or API surface.

How Orbis AppSec Detected This

  • Source: User-influenced or pipeline-provided command strings passed to concurrently's command runner.
  • Sink: shell-quote@1.8.3's internal call to brace-expansion's expand() function, which processes the command string without complexity bounds.
  • Missing control: No upper bound on the number of brace-expansion combinations that brace-expansion would attempt to generate; no input validation before the expansion step.
  • CWE: CWE-1333 — Inefficient Regular Expression Complexity (Algorithmic Complexity / Catastrophic Expansion).
  • Fix: Upgraded concurrently from 9.2.1 to 9.2.4 in both package.json and yarn.lock, which transitively replaces shell-quote@1.8.3 with shell-quote@1.9.0 containing a patched brace-expansion.

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 JavaScript ecosystem's deep dependency trees create an ever-present surface for algorithmic complexity attacks. A package as innocuous as brace-expansion—used for shell glob matching—can become a denial-of-service vector when it lacks bounds on combinatorial expansion. The fix here was surgical: a two-file change (package.json + yarn.lock) that bumps concurrently by two patch versions and swaps out the vulnerable shell-quote@1.8.3 for the patched 1.9.0.

The broader lesson is that security hygiene for modern JavaScript projects requires treating the entire dependency tree—not just direct dependencies—as part of your attack surface. Automated scanning and automated fix PRs are no longer optional; they are the only practical way to stay ahead of vulnerabilities that hide three levels deep in a yarn.lock file.


References

Frequently Asked Questions

What is a brace-expansion denial of service?

It is an algorithmic complexity attack where a specially crafted string containing nested or repeated brace patterns (e.g., `{a,b}{c,d}...`) causes the brace-expansion library to perform an exponentially growing number of string combinations, consuming CPU until the process hangs or crashes.

How do you prevent brace-expansion DoS in JavaScript?

Keep brace-expansion and packages that depend on it (such as shell-quote and concurrently) up to date, validate and limit the length and structure of any user-supplied strings before passing them to shell-quoting or glob-expansion functions, and use automated dependency scanning tools to catch vulnerable transitive dependencies.

What CWE is brace-expansion DoS?

CWE-1333, Inefficient Regular Expression Complexity (also sometimes catalogued under CWE-400, Uncontrolled Resource Consumption), because the vulnerability stems from an algorithm whose time complexity grows exponentially with input size.

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

It significantly reduces the risk but is not sufficient on its own. Even moderately short strings with deeply nested braces can trigger exponential expansion. The most reliable fix is upgrading to a patched version of brace-expansion.

Can static analysis detect brace-expansion DoS?

Yes. Tools like Trivy, Snyk, and npm audit can flag known-vulnerable versions of brace-expansion in a dependency tree. Orbis AppSec used Trivy to detect this exact pattern in the yarn.lock file and automatically generated a fix PR.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5306

Related Articles

high

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability in `shell-quote` 1.8.3 (CVE-2026-9277) allowed arbitrary code execution through unescaped line terminators in shell arguments. The fix upgrades the dependency to `shell-quote` 1.8.4 via a pnpm override, closing the attack surface in both `launch-editor` and `react-dev-utils` dependency chains.

critical

How Command Injection happens in Python subprocess calls and how to fix it

A critical OS command injection vulnerability was discovered in `backend_android.py`, where the `_sendevent` function constructed shell commands using f-string interpolation with a user-controlled `dev` parameter and executed them with `shell=True`. An attacker could exploit this by sending a crafted `android-config` packet with a malicious `eventDev` value containing shell metacharacters, enabling arbitrary command execution on the host. The fix validates the `dev` parameter against a strict re

critical

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote 1.8.3, where unescaped line terminators could allow arbitrary code execution by bypassing the library's shell argument quoting logic. The fix upgrades shell-quote to version 1.8.4 and pins the dependency via a package.json override to ensure the patched version is consistently resolved across the dependency tree. This matters because shell-quote is widely used in Node.js tooling to safely construct shell com

high

How Command Injection happens in Node.js child_process calls and how to fix it

A high-severity command injection vulnerability was discovered in `src/cli/commands/extract.js` at line 257, where user-controlled input was passed unsanitized into a `child_process` call via the `extractZipWithSystemTool` function. The fix eliminates the dangerous shell execution path entirely by removing the `spawn`-based system tool invocation and relying on the safe, pure-JavaScript `yauzl` library for ZIP extraction. This proactive hardening prevents downstream consumers of this Node.js lib

critical

How Remote Code Execution via Security Fix Bypass happens in Node.js and how to fix it

CVE-2026-28292 is a critical Remote Code Execution vulnerability in the simple-git npm package that allowed attackers to bypass previously shipped security patches. The flaw affected applications using simple-git versions prior to 3.32.3, and was resolved by upgrading to 3.36.0, which introduced a dedicated argument-parsing architecture to properly sanitize untrusted input before it reaches the underlying git process.

high

How Path Traversal happens in PostCSS Source Map Loading and how to fix it

A path traversal vulnerability in PostCSS versions before 8.5.18 allowed malicious `sourceMappingURL` comments in CSS files to trick PostCSS into loading arbitrary `.map` files from the filesystem. The fix upgrades PostCSS from 8.5.15 to 8.5.18 in `frontend/package-lock.json` and pins the version via an override in `frontend/package.json`, closing the file disclosure vector before it could be chained with other weaknesses.