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
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed 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 concurrently → shell-quote → brace-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.


Key Takeaways

  • Transitive dependencies carry real risk: The vulnerability was two levels deep (concurrently → shell-quote → brace-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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5306

Related Articles

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.

critical

docker_rpc.uc Command Injection: Unsanitized RPC Parameters

A critical command injection vulnerability in the Docker RPC handler allowed authenticated attackers to execute arbitrary system commands by injecting shell metacharacters into container ID, port, user ID, or command parameters. The fix validates all user-supplied inputs against strict whitelist patterns before interpolating them into shell commands.