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 before 1.8.4) caused by improper handling of unescaped line terminators, which could allow attackers to inject and execute arbitrary shell commands. The fix upgrades `shell-quote` from 1.8.1 to 1.8.4 across `package.json`, `package-lock.json`, and `yarn.lock`, and pins the version using both `overrides` and `resolutions` to ensure no transitive dependency pulls in the vulnerable version. This i

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. The flaw stems from unescaped line terminators (`\n`, `\r`) in shell argument strings that bypass quoting logic, enabling arbitrary shell command execution. The fix is to upgrade `shell-quote` to 1.8.4 and pin it via `overrides` in `package.json` and `resolutions` in `yarn.lock` to prevent vulnerable transitive versions from being resolved. Any Node.js project that passes user-controlled input through `shell-quote` and then executes the result in a shell is directly exploitable.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade shell-quote to 1.8.4 and pin with package.json overrides and yarn.lock resolutions
riskArbitrary shell command execution by injecting newline characters into quoted arguments
languageJavaScript / Node.js
root causeshell-quote 1.8.1 did not escape `\n` and `\r` characters, allowing them to terminate a quoted argument and inject new shell commands
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
Vulnerable version 1.8.1 (and earlier)
Fixed version 1.8.4
CWE CWE-78 — OS Command Injection
Scanner Trivy

Introduction

The shell-quote library is one of those quiet workhorses of the Node.js ecosystem — it sits deep in dependency trees, doing the unglamorous job of turning arrays of strings into properly quoted shell commands. Thousands of tools rely on it: build scripts, linters, test runners, and CI utilities all pass arguments through it before handing them to a shell. That trust makes CVE-2026-9277 particularly dangerous.

In this project's package-lock.json, Trivy flagged shell-quote pinned at version 1.8.1 with a critical severity finding. The root cause: version 1.8.1 does not escape line terminator characters (\n, \r) when quoting shell arguments. An attacker who can influence any string that eventually passes through shell-quote and into a shell can use a bare newline to escape the quoted context and inject arbitrary commands — no exotic bypass required.


The Vulnerability Explained

What shell-quote Does

shell-quote exposes two main functions:

  • quote(args) — takes an array of strings and returns a single, safely-quoted shell command string.
  • parse(cmd) — parses a shell command string back into tokens.

The intended guarantee of quote() is that even if an argument contains shell metacharacters ($, `, ", ', ;, &, |, etc.), they will be escaped so the shell treats the entire value as a single literal argument. Version 1.8.1 upholds this guarantee for most metacharacters — but not for newlines.

The Unescaped Line Terminator Bug

Consider this simplified but representative usage pattern:

const { quote } = require('shell-quote'); // version 1.8.1
const { execSync } = require('child_process');

// userInput comes from an HTTP request parameter, CLI argument, etc.
function processFile(userInput) {
  const cmd = `cat ${quote([userInput])}`;
  execSync(cmd, { shell: true });
}

In shell-quote 1.8.1, the quote() function wraps strings in single quotes and escapes embedded single quotes, but it does not strip or escape \n (newline, 0x0A) or \r (carriage return, 0x0D).

A shell interprets a newline as a command separator — functionally identical to a semicolon. So if userInput is:

report.txt\nrm -rf /tmp/important

Then quote(['report.txt\nrm -rf /tmp/important']) in version 1.8.1 produces something like:

'report.txt
rm -rf /tmp/important'

When the shell evaluates this, the newline inside the single-quoted string terminates the first command and begins a new one. The result is two commands executing:

  1. cat 'report.txt ← malformed but executed
  2. rm -rf /tmp/important' ← the injected command (with a trailing quote that most shells tolerate or ignore)

This is a textbook CWE-78 injection: user-controlled data containing a special character (\n) that the sanitizer failed to neutralize reaches a shell execution sink.

Real-World Attack Surface

The severity is amplified by how shell-quote is consumed. It is a transitive dependency of tools like jest, webpack, and various ESLint plugins. Any build or test pipeline that:

  1. Accepts user-supplied filenames, branch names, commit messages, or environment variables, and
  2. Passes those values through shell-quote into child_process.exec(), execSync(), or any { shell: true } variant

…is exploitable. In CI/CD environments where build scripts run with elevated permissions, successful exploitation could mean arbitrary code execution on the build host, secret exfiltration, or supply-chain compromise.


The Fix

What Changed in the Dependency Files

The fix consists of three coordinated changes across the lock files and package manifest:

1. package-lock.json — Version bump

 "node_modules/shell-quote": {
-  "version": "1.8.1",
-  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz",
-  "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==",
+  "version": "1.8.4",
+  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+  "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
   "license": "MIT",
+  "engines": {
+    "node": ">= 0.4"
+  },
   "funding": {
     "url": "https://github.com/sponsors/ljharb"
   }

The integrity hash changes from the 1.8.1 tarball to the 1.8.4 tarball. npm verifies this hash on install, so any tampered package would be rejected. The addition of the engines field is also a signal that 1.8.4 includes updated metadata.

2. package.json — Overrides and resolutions pinning

+    "shell-quote": "1.8.4"
   },
   "engines": {
     "node": ">=18"
+  },
+  "resolutions": {
+    "shell-quote": "1.8.4"
   }

This is the critical defense-in-depth step. Without pinning, any transitive dependency that declares shell-quote: "^1.8.1" in its own package.json could still resolve to the vulnerable version when the lock file is regenerated or when running npm install in a fresh environment. The overrides field (npm 8.3+) and the resolutions field (Yarn) force all resolutions of shell-quote anywhere in the dependency tree to use exactly 1.8.4.

3. yarn.lock — Lock file update

The yarn.lock entry for shell-quote is updated to match the new resolved URL and integrity hash, ensuring Yarn-based installs are equally protected.

Why Version 1.8.4 Fixes It

In shell-quote 1.8.4, the quote() function's internal character-escaping logic was updated to treat \n and \r as characters requiring neutralization — either by escaping them or by encoding the argument in a way the shell cannot misinterpret as a command boundary. This closes the injection vector at the library level.


Prevention & Best Practices

1. Prefer Argument Arrays Over Shell Strings

The safest approach is to never construct a shell string at all:

// ❌ Vulnerable pattern — shell string construction
const { execSync } = require('child_process');
execSync(`cat ${quote([userInput])}`, { shell: true });

// ✅ Safe pattern — argument array, no shell interpolation
const { execFileSync } = require('child_process');
execFileSync('cat', [userInput]); // shell metacharacters are irrelevant

execFile / execFileSync / spawn with an explicit argument array bypass the shell entirely. No quoting library is needed, and no quoting library bug can affect you.

2. Validate Inputs Before They Reach Shell Code

If you must use a shell string, validate that inputs do not contain line terminators or other shell metacharacters before passing them to quote():

function sanitizeShellArg(input) {
  if (/[\n\r]/.test(input)) {
    throw new Error('Input contains illegal line terminator characters');
  }
  return input;
}

This is defense-in-depth: even with a patched library, rejecting obviously malicious input early reduces blast radius.

3. Pin Transitive Dependencies

As demonstrated in this fix, use overrides (npm) and resolutions (Yarn) to pin security-sensitive transitive dependencies:

// package.json
{
  "overrides": {
    "shell-quote": "1.8.4"
  },
  "resolutions": {
    "shell-quote": "1.8.4"
  }
}

This prevents a future npm install or lock file regeneration from silently downgrading to a vulnerable version.

4. Run Dependency Audits in CI

Add npm audit --audit-level=high or a Trivy scan to your CI pipeline. CVE-2026-9277 was detected by Trivy scanning package-lock.json — a step that takes seconds and would have caught this before it reached production.

5. Relevant Standards


Key Takeaways

  • shell-quote 1.8.1's quote() function did not escape \n or \r, meaning any user-controlled string containing a newline could inject a second shell command regardless of other escaping.
  • The package-lock.json integrity hash is your last line of defense against a tampered package — the hash change from 1.8.1 to 1.8.4 is cryptographically verifiable proof that a different, patched tarball is now installed.
  • Pinning with overrides and resolutions in package.json is essential — without it, transitive dependencies can silently re-introduce the vulnerable version on the next npm install.
  • execFile / spawn with argument arrays is categorically safer than exec / execSync with shell strings, because it eliminates the shell parsing step entirely.
  • Trivy scanning package-lock.json caught this — static SCA (Software Composition Analysis) on lock files is a practical, low-overhead control that surfaces vulnerabilities in transitive dependencies that developers rarely inspect manually.

How Orbis AppSec Detected This

  • Source: User-influenced string values (filenames, branch names, environment variables, CLI arguments) passed as arguments to shell-quote's quote() function.
  • Sink: The quoted string returned by quote() consumed by child_process.exec(), execSync(), or any Node.js API invoked with { shell: true } — where the shell interprets the unescaped \n as a command separator.
  • Missing control: shell-quote 1.8.1 performed no escaping or rejection of line terminator characters (\n, \r) inside quoted argument strings, leaving a complete bypass of its own quoting guarantee.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: Upgraded shell-quote from 1.8.1 to 1.8.4 in package-lock.json, added a version pin under overrides in package.json, and updated yarn.lock to enforce the patched version across all dependency resolution paths.

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 guarantees are only as strong as their most obscure edge case. shell-quote correctly escaped dozens of shell metacharacters — but missing just two (\n and \r) was enough to invalidate the entire safety contract and open the door to arbitrary code execution. The fix is straightforward: upgrade to 1.8.4 and pin the version so transitive dependency resolution cannot undo the upgrade. More broadly, prefer execFile with argument arrays over shell string construction whenever possible, and integrate SCA scanning into CI so vulnerabilities in the dependency tree are caught before they reach production.


References

Frequently Asked Questions

What is command injection via unescaped line terminators?

It is an attack where a newline or carriage-return character embedded in a shell argument breaks out of the quoted string context, allowing an attacker to append and execute arbitrary shell commands on the same line or the next.

How do you prevent command injection in Node.js shell argument handling?

Always use a well-maintained, up-to-date shell quoting library like shell-quote ≥1.8.4, validate and reject inputs containing line terminators before passing them to shell commands, and prefer child_process.execFile() with explicit argument arrays over shell string construction.

What CWE is command injection?

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

Is escaping special characters enough to prevent command injection?

Only if the escaping logic is complete. The shell-quote 1.8.1 bug shows that missing even one class of special characters (newlines) is enough to break the entire safety guarantee. Defense-in-depth — input validation plus a patched library plus avoiding shell=true patterns — is the right approach.

Can static analysis detect command injection in shell-quote usage?

Yes. Trivy flagged CVE-2026-9277 in the dependency tree via its vulnerability database. Semgrep rules targeting tainted data flowing into shell string builders can also surface this pattern at the code level.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #18

Related Articles

high

How Command Injection Happens in Node.js Route Handlers and How to Fix It

A high-severity command injection vulnerability was discovered in `webhook/src/routes/bid-requests/create.route.js`, where user-controlled values were passed directly to route handlers without any schema validation. Without input validation, attackers could supply malformed or malicious values — including shell metacharacters — that propagate into downstream command construction, enabling arbitrary command execution. The fix adds strict UUID and type validation middleware directly in the route d

high

How Child Process Command Injection happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `src/account_manager.js`, where user-controllable input was passed directly to Node.js's `child_process` without sanitization. Alongside this, the companion `src/keyring_helper.py` GNOME Keyring helper lacked any execution guard, meaning any local user could invoke it to read, write, or delete stored OAuth tokens. The fix adds an OS-level ownership check that restricts execution of the keyring helper to the script's owner only.

critical

How Command Injection happens in Node.js CLI scripts and how to fix it

A Node.js CLI script in `scripts/refresh-htv-signature.js` accepted a user-controlled `slug` argument from `process.argv` and interpolated it directly into a URL string without any validation. While the immediate usage was an HTTP request via `axios.get()`, the absence of input sanitization created a pathway for command injection in current and future code paths. The fix adds a strict allowlist regex that rejects any slug not matching `[a-zA-Z0-9_-]+` before it can reach any downstream operation

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 attackers to inject and execute arbitrary shell commands. The fix upgrades the dependency to shell-quote 1.8.4 and pins the version using npm's `overrides` field to ensure no transitive dependency can reintroduce the vulnerable version. This type of vulnerability is particularly dangerous in Node.js toolchains where shell-quote is used to safely construct s

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 `js/cu_linux_executor.js`, where `child_process.execSync()` was used to run shell commands with potentially unsanitized input. The fix replaces shell-based execution with `execFileSync()`, which spawns processes directly without invoking a shell, eliminating the possibility of shell metacharacter injection. This change is a critical defensive hardening step that removes an exploit primitive that could be chained with other weaknes

critical

How Command Injection happens in Python subprocess calls and how to fix it

A critical command injection vulnerability in `host/beectl-py2.py` allowed attackers to pass arbitrary subprocess arguments through a browser extension's JSON configuration, enabling execution of malicious shell commands on the host machine. The fix introduces two new validation functions — `sanitize_args()` and `sanitize_ext()` — that enforce strict type and content constraints on user-controlled input before it reaches the `subprocess` call. This change closes a direct path from browser extens