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 could allow arbitrary code execution by bypassing the library's shell argument quoting logic. The fix upgrades shell-quote to version 1.8.4 and pins the dependency via a package.json override to ensure the patched version is consistently resolved across the dependency tree. This matters because shell-quote is widely used in Node.js tooling to safely construct shell com

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) for Node.js. The flaw arises because shell-quote 1.8.3 failed to escape certain line terminator characters (such as `\n` and `\r`), allowing an attacker who controls input to inject additional shell commands beyond the intended quoted argument. The fix is to upgrade shell-quote to 1.8.4 and, if the package appears as a transitive dependency, add an `overrides` entry in `package.json` to force resolution to the patched version.

Vulnerability at a Glance

cweCWE-78
fixUpgrade shell-quote to 1.8.4 and pin via package.json overrides
riskArbitrary shell command execution by an attacker who controls quoted input
languageJavaScript / Node.js
root causeshell-quote 1.8.3 did not escape newline/line-terminator characters, allowing injection of additional shell commands
vulnerabilityCommand Injection via unescaped line terminators

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


At a Glance

Field Detail
CVE CVE-2026-9277
Severity Critical
Package shell-quote (npm)
Affected version 1.8.3 and earlier
Fixed version 1.8.4
Root cause Unescaped line terminators enabling shell command injection
CWE CWE-78: OS Command Injection

Introduction

The package-lock.json in this project locked shell-quote to version 1.8.3 — a version that contains a critical flaw in its core purpose: safely quoting shell arguments. When a library whose entire job is to prevent command injection is itself vulnerable to command injection, the consequences ripple through every application that trusts it to sanitize user input before passing strings to the shell.

Trivy's scanner flagged rule CVE-2026-9277 against this exact version, identifying that the library failed to escape line terminator characters (\n, \r, and similar Unicode line endings). This is not a theoretical edge case. Any code path in your application that takes user-influenced input, passes it through shell-quote, and then hands the result to a shell executor is potentially exploitable — regardless of how carefully the rest of your code is written.


The Vulnerability Explained

What shell-quote Does (and Why It Matters)

shell-quote is an npm package used to safely construct shell command strings from arrays of arguments. A typical usage looks like this:

const quote = require('shell-quote').quote;
const userInput = req.query.filename;
const cmd = `cat ${quote([userInput])}`;
exec(cmd, callback);

The intention is that quote() will escape any dangerous characters in userInput so that the resulting cmd is safe to pass to a shell. For most characters — spaces, semicolons, backticks, dollar signs — version 1.8.3 does this correctly.

The Flaw: Unescaped Line Terminators

The vulnerability in 1.8.3 is that line terminator characters are not treated as special. Shell interpreters (bash, sh, zsh) treat a newline (\n) as a command separator — functionally equivalent to a semicolon. If an attacker can inject a literal newline character into input that is subsequently passed through shell-quote, the quoted output will contain an unescaped newline, and the shell will interpret everything after it as a new, separate command.

Consider this attack input:

innocent_file.txt\nrm -rf /tmp/important

In shell-quote 1.8.3, this would be quoted in a way that preserves the literal newline, producing something like:

cat 'innocent_file.txt'
rm -rf /tmp/important

The shell sees two commands and executes both. The attacker has achieved arbitrary command execution with the privileges of the Node.js process — without ever breaking out of a quoted string in the traditional sense.

Why This Is Critical

The severity is critical (not just high) because:

  1. No authentication bypass required — any input vector that reaches the quoting call is sufficient.
  2. The fix location is a trusted library — developers who use shell-quote do so specifically to avoid writing their own escaping logic. A flaw here undermines the entire security model.
  3. Line terminators are easy to inject — HTTP query parameters, form fields, JSON body values, file names from uploads — all of these can contain \n or \r\n unless explicitly stripped upstream.

The Fix

What Changed in the Dependency

The fix required two coordinated changes: updating the resolved version of shell-quote in package-lock.json, and adding an explicit overrides entry in package.json to ensure the patched version is used regardless of what other packages in the dependency tree might request.

package-lock.json — Version and Integrity Update

 "node_modules/shell-quote": {
-  "version": "1.8.3",
-  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
-  "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+  "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"

The integrity hash change is significant — it is a cryptographic guarantee (SHA-512) that the downloaded package matches the expected content. Changing this hash confirms that the installed artifact is genuinely the new 1.8.4 release and not a re-tagged version of 1.8.3.

package.json — Pinning via overrides

 "overrides": {
-  "picomatch": "^4.0.3"
+  "picomatch": "^4.0.3",
+  "shell-quote": "1.8.4"
 }

This is the more important of the two changes from a security maintenance perspective. shell-quote may appear as a transitive dependency — pulled in by other packages rather than directly by the application. Without the overrides entry, npm's dependency resolution could install 1.8.3 for a nested package even after package-lock.json has been updated at the top level. The overrides field forces npm to resolve all instances of shell-quote in the dependency tree to 1.8.4, closing the vulnerability regardless of where in the tree it appears.

How 1.8.4 Fixes the Escaping

Version 1.8.4 adds line terminator characters to the set of characters that trigger quoting or escaping in the output. Specifically, \n, \r, and potentially other Unicode line separators are now treated as unsafe characters that must be escaped before they appear in a quoted shell argument. This means the attack payload described earlier — innocent_file.txt\nrm -rf /tmp/important — would produce output where the newline is escaped and the shell sees it as part of the filename argument, not as a command separator.


Prevention & Best Practices

1. Keep Shell-Handling Dependencies Pinned and Audited

Any package that touches shell construction is in your application's security critical path. These dependencies deserve:

  • Explicit version pinning (not ^ or ~ ranges alone)
  • Regular npm audit runs in CI
  • Automated vulnerability scanning (Trivy, Snyk, or similar) on every pull request

2. Prefer Array-Based Process APIs

Where possible, avoid constructing shell strings altogether. Node.js's child_process.execFile() and child_process.spawn() (without shell: true) accept argument arrays and bypass the shell entirely:

// Vulnerable pattern — passes through shell
const { exec } = require('child_process');
exec(`cat ${quote([userInput])}`, callback);

// Safer pattern — no shell involved
const { execFile } = require('child_process');
execFile('cat', [userInput], callback);

When you use execFile or spawn with an arguments array, line terminators in the input are passed as literal data to the process — the shell never sees them.

3. Validate and Sanitize Input at the Boundary

Even with a patched shell-quote, consider stripping or rejecting line terminator characters at the point where user input enters your system:

function sanitizeForShell(input) {
  // Reject inputs containing line terminators before they reach shell construction
  if (/[\n\r\u2028\u2029]/.test(input)) {
    throw new Error('Invalid input: line terminators not permitted');
  }
  return input;
}

This is defense in depth — the library fix is the primary control, but input validation at the boundary provides a second layer.

4. Use npm overrides for Transitive Dependency Security

As demonstrated in this fix, npm's overrides field (introduced in npm 8.3) is a powerful tool for enforcing minimum safe versions across the entire dependency tree:

"overrides": {
  "shell-quote": "1.8.4"
}

Make this part of your standard response playbook whenever a transitive dependency is flagged with a critical CVE.

5. Reference Standards


Key Takeaways

  • shell-quote 1.8.3 does not escape \n and \r — any application passing user input through this version and into a shell executor is vulnerable to command injection, regardless of other safeguards.
  • Upgrading the direct dependency is not always enough — the overrides entry in package.json is required to patch transitive instances of shell-quote that other packages in your tree may pull in independently.
  • The integrity hash in package-lock.json is a security control — the change from the 1.8.3 SHA-512 to the 1.8.4 SHA-512 provides cryptographic assurance that the correct artifact is installed.
  • Line terminators are valid command separators in most shells — escaping libraries must treat \n, \r, and Unicode line separators (U+2028, U+2029) as dangerous characters, not just the "classic" injection characters like ;, |, and backticks.
  • child_process.execFile() with an argument array eliminates this entire class of vulnerability for new code — prefer it over exec() with shell string construction wherever feasible.

How Orbis AppSec Detected This

  • Source: User-influenced input flowing into shell command construction via the shell-quote quoting function
  • Sink: The shell-quote quote function in node_modules/shell-quote, called before passing constructed strings to shell executors such as child_process.exec()
  • Missing control: shell-quote 1.8.3 did not include line terminator characters (\n, \r, U+2028, U+2029) in its set of characters requiring escaping, allowing them to pass through unmodified into shell command strings
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
  • Fix: Upgraded shell-quote from 1.8.3 to 1.8.4 in package-lock.json and added a "shell-quote": "1.8.4" entry to the overrides block in package.json to enforce the patched version across the full dependency tree

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 libraries are not immune to security vulnerabilities. shell-quote exists specifically to make shell command construction safe — yet a single missing character class in its escaping logic created a critical command injection vector. The fix is straightforward: upgrade to 1.8.4 and use npm overrides to ensure the patch applies everywhere in your dependency tree. But the broader lesson is architectural: the safest code is code that never reaches the shell in the first place. Where you can use execFile with argument arrays instead of exec with shell strings, do so. Layer your defenses — patched libraries, input validation at boundaries, and shell-free process APIs — so that no single library flaw can become a system compromise.


References

Frequently Asked Questions

What is command injection?

Command injection occurs when untrusted input is incorporated into a shell command without proper escaping, allowing an attacker to append or substitute their own commands and execute arbitrary code on the host system.

How do you prevent command injection in Node.js?

Use libraries like shell-quote (kept up to date) to safely escape arguments, prefer APIs that accept argument arrays instead of shell strings (e.g., child_process.execFile), validate and allowlist input, and never pass raw user input to shell=true invocations.

What CWE is command injection?

Command injection is classified as 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 itself correctly handles all special characters, including line terminators like \n and \r. CVE-2026-9277 is a direct example of a quoting library that was insufficient — upgrading to a patched version is required.

Can static analysis detect command injection?

Yes. Tools like Trivy (which flagged this CVE), Semgrep, and Snyk can identify vulnerable dependency versions and dangerous shell-construction patterns. Trivy detected CVE-2026-9277 by matching the shell-quote version in package-lock.json against its vulnerability database.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #53

Related Articles

high

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.

critical

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

A critical OS command injection vulnerability was discovered in `backend_android.py`, where the `_sendevent` function constructed shell commands using f-string interpolation with a user-controlled `dev` parameter and executed them with `shell=True`. An attacker could exploit this by sending a crafted `android-config` packet with a malicious `eventDev` value containing shell metacharacters, enabling arbitrary command execution on the host. The fix validates the `dev` parameter against a strict re

high

How Denial of Service via Brace Expansion Happens in JavaScript and How to Fix It

A high-severity denial-of-service vulnerability (CVE-2026-13149) in the `brace-expansion` package was fixed by upgrading `concurrently` from `^9.2.1` to `^9.2.4`, which pulls in `shell-quote 1.9.0` instead of the vulnerable `1.8.3`. The flaw allowed an attacker to craft a specially formed brace-expansion pattern that caused exponential processing time, potentially hanging Node.js processes. Left unpatched, any code path that passed user-influenced strings through `concurrently`'s shell-quoting l

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 `src/cli/commands/extract.js` at line 257, where user-controlled input was passed unsanitized into a `child_process` call via the `extractZipWithSystemTool` function. The fix eliminates the dangerous shell execution path entirely by removing the `spawn`-based system tool invocation and relying on the safe, pure-JavaScript `yauzl` library for ZIP extraction. This proactive hardening prevents downstream consumers of this Node.js lib

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 npm package that allowed attackers to bypass previously shipped security patches. The flaw affected applications using simple-git versions prior to 3.32.3, and was resolved by upgrading to 3.36.0, which introduced a dedicated argument-parsing architecture to properly sanitize untrusted input before it reaches the underlying git process.

critical

How Heap Buffer Overflows Happen in C++ ZIP Extraction and How to Fix Them

A critical heap buffer overflow vulnerability was discovered in `TKLiveSync/unzip.cpp`, where ZIP archive entry names were copied into a `PATH_MAX`-sized heap buffer using `strcpy()` without any length validation. Since the ZIP specification allows entry names up to 65,535 bytes — far exceeding typical `PATH_MAX` values of 1,024 to 4,096 bytes — a crafted archive could overflow the buffer and corrupt heap memory. The fix replaces the unsafe `strcpy`/`dirname` pattern with `std::string` operation