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.


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.


Prevention and further reading

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 child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

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

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.

high

How command injection happens in JavaScript/Node.js and how to fix it

A build script in a Node.js library used `child_process.exec()` with template-literal-interpolated commit hashes to generate SVG diffs, creating a command injection primitive. The fix replaces `exec()` with `execFile()` and adds strict regex validation of commit hashes before they're used in any shell command.