Back to Blog
critical SEVERITY7 min read

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

CVE-2026-9277 is a critical command injection vulnerability in shell-quote versions before 1.9.0 that allows arbitrary code execution through unescaped line terminators in user-controlled input. The vulnerability was fixed by upgrading from version 1.8.2 to 1.9.0, which properly escapes newline and carriage return characters to prevent shell metacharacter injection. This fix is essential for any Node.js application that uses shell-quote to safely parse shell arguments.

O
By Orbis AppSec
Published September 7, 2026Reviewed September 7, 2026

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability in the Node.js shell-quote library (versions before 1.9.0) caused by improper handling of line terminator characters (newlines and carriage returns) when escaping shell arguments. Attackers could inject arbitrary shell commands by including unescaped line terminators in user input, bypassing shell-quote's escaping logic. The fix, released in shell-quote 1.9.0, properly escapes all line terminators to prevent command injection, and the patch involves upgrading the dependency in package-lock.json and adding an override in package.json to enforce the patched version across the dependency tree.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade shell-quote to 1.9.0, which adds proper escaping for all line terminator characters
riskArbitrary code execution with application privileges; complete system compromise possible
languageJavaScript/Node.js
root causeshell-quote 1.8.2 failed to escape newline and carriage return characters, allowing injection of shell commands on new lines
vulnerabilityCommand Injection via Unescaped Line Terminators

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

Introduction

In a recent security audit, Trivy's vulnerability scanner flagged a critical issue in the dependency tree: shell-quote version 1.8.2 contained CVE-2026-9277, a command injection vulnerability that could allow attackers to execute arbitrary shell commands. The vulnerability exists in how shell-quote processes user-controlled input when escaping arguments for shell execution. Specifically, shell-quote 1.8.2 failed to properly escape line terminator characters—newlines (\n) and carriage returns (\r)—which allowed attackers to inject commands that would execute on separate shell command lines, completely bypassing the library's escaping logic.

This matters because shell-quote is a common utility in Node.js applications that need to safely convert JavaScript arrays into properly escaped shell command strings. Developers rely on it to prevent shell injection when building commands programmatically. A flaw in this critical function creates a silent, exploitable vulnerability in any application using the vulnerable version.

The Vulnerability Explained

What Is shell-quote and Why Does It Matter?

The shell-quote library solves a fundamental problem in Node.js development: converting JavaScript arrays of arguments into a single shell command string with proper escaping. For example:

// Without shell-quote, this is unsafe:
const userInput = "file.txt; rm -rf /";
const command = `cat ${userInput}`; // Dangerous!
// This would execute: cat file.txt; rm -rf /

// With shell-quote, you get safe escaping:
const quote = require('shell-quote');
const safe = quote.quote([userInput]); 
// Before the fix: "file.txt; rm -rf /" (if input contains \n)
// After the fix: "file.txt; rm -rf /" (properly escaped)

The problem emerged when user input contained line terminator characters that weren't being escaped by version 1.8.2.

The Specific Vulnerability

In shell-quote 1.8.2, the escaping logic had a critical blind spot: it didn't escape newline (\n) and carriage return (\r) characters. An attacker could craft input like this:

const maliciousInput = "file.txt\nrm -rf /tmp/important";
const command = quote.quote([maliciousInput]);
// In version 1.8.2, this would produce output that allows:
// cat file.txt
// rm -rf /tmp/important
// Both commands execute!

When this escaped string is passed to a shell (via child_process.exec() or similar), the shell interprets the unescaped newline as a command separator, allowing the attacker to inject a second, completely different command. The shell sees it as:

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

Which the shell parses as two separate commands on two lines.

Real-World Attack Scenario

Imagine a Node.js application that processes file uploads and generates a thumbnail:

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

app.post('/upload', (req, res) => {
  const filename = req.body.filename; // User-controlled!
  const command = `convert ${quote.quote([filename])} thumb.png`;

  exec(command, (error, stdout) => {
    res.send('Thumbnail created');
  });
});

An attacker uploads a file with a name like:

image.jpg\nrm -rf /var/www/html/*

The vulnerable shell-quote 1.8.2 fails to escape the \n, and the resulting command becomes:

convert 'image.jpg
rm -rf /var/www/html/*' thumb.png

The shell executes both the convert command (which may fail) and the destructive rm -rf command. The application's entire web directory is deleted.

Why This Is Critical

  • Direct RCE: Attackers can execute arbitrary commands with the application's privileges
  • Silent bypass: The application appears to call shell-quote safely, but the vulnerability silently bypasses the protection
  • Privilege escalation: If the Node.js process runs with elevated privileges, attackers gain those privileges
  • Data breach: Attackers can exfiltrate sensitive data, modify records, or install backdoors

The Fix

The fix involves upgrading shell-quote from version 1.8.2 to version 1.9.0, which was released to address this exact vulnerability. The upgrade is applied in two places in your dependency configuration:

Change 1: Update package-lock.json

     "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==",
       "license": "MIT",
       "peer": true,
       "engines": {

The new version (1.9.0) includes proper escaping for line terminator characters. The integrity hash (sha512-Iov+...) changes because the code has been patched.

Change 2: Add Override in package.json

   "engines": {
     "node": ">=18.0"
+  },
+  "overrides": {
+    "shell-quote": "1.9.0"
   }

The overrides field (a Node.js/npm feature) ensures that shell-quote 1.9.0 is used throughout the entire dependency tree, even if other packages depend on older versions. This prevents transitive dependencies from pulling in the vulnerable version.

What Changed Inside shell-quote 1.9.0

While we don't have the exact source diff, the fix addresses the specific vulnerability by ensuring that the escaping function now properly escapes all line terminator characters:

Before (1.8.2 - Vulnerable):

// Pseudo-code showing the gap
function escape(str) {
  // Escapes: ;, |, &, >, <, (, ), etc.
  // MISSING: escape for \n and \r characters!
  return str.replace(/[;&|><()]/g, '\\$&');
}

After (1.9.0 - Fixed):

// Pseudo-code showing the fix
function escape(str) {
  // Now escapes: ;, |, &, >, <, (, ), \n, \r, etc.
  return str.replace(/[\n\r;&|><()]/g, '\\$&');
}

Verification

After applying this upgrade, test that shell-quote properly escapes line terminators:

const quote = require('shell-quote'); // Now 1.9.0

const maliciousInput = "file.txt\nrm -rf /";
const escaped = quote.quote([maliciousInput]);
console.log(escaped);
// Output should properly escape the newline
// The command injection is prevented

Prevention & Best Practices

1. Keep Dependencies Updated

  • Run npm audit regularly to identify vulnerable dependencies
  • Configure automated dependency scanning (GitHub Dependabot, Snyk, Trivy)
  • Pin major versions but allow patch updates: "shell-quote": "~1.9.0"

2. Avoid Shell Execution When Possible

Instead of:

const { exec } = require('child_process');
exec(`ls -la ${filename}`); // Never do this, even with escaping

Use:

const { execFile } = require('child_process');
execFile('ls', ['-la', filename]); // Arguments as separate array elements

With execFile(), arguments are passed directly to the program without shell interpretation, eliminating the entire class of shell injection vulnerabilities.

3. Input Validation

Even with proper escaping, validate user input:

// Validate filename format
if (!/^[\w\-. ]+$/.test(filename)) {
  throw new Error('Invalid filename');
}

4. Use Security Linters

Tools like ESLint with security plugins can flag dangerous patterns:
- eslint-plugin-security flags exec() usage
- semgrep can detect shell injection patterns

5. Static Analysis in CI/CD

Integrate Trivy or similar tools into your CI/CD pipeline:

- name: Scan for vulnerabilities
  run: trivy fs . --severity CRITICAL,HIGH

6. Review Related Code

Search your codebase for other uses of shell-quote with user input to ensure they're all protected by the updated version.

Key Takeaways

  • Line terminators are shell metacharacters: shell-quote 1.8.2's failure to escape \n and \r was a critical oversight that completely bypassed its security guarantees for certain inputs.

  • Transitive dependencies matter: The overrides field in package.json ensures that even indirect dependencies of shell-quote use the patched version, preventing vulnerable versions from being installed.

  • Always use argument arrays when possible: execFile() with argument arrays is safer than exec() with string concatenation, even when using escaping libraries.

  • Dependency scanning catches what code review misses: Trivy's vulnerability scanner detected this issue automatically; without automated scanning, this vulnerability could remain in production indefinitely.

  • Escaping is necessary but not sufficient: shell-quote is one layer of defense, but you should combine it with input validation, avoiding shell execution entirely, and regular security audits.

How Orbis AppSec Detected This

Source: Line terminator characters embedded in user-controlled input to shell-quote's quote() function

Sink: shell-quote 1.8.2's escaping logic in the quote() function, which failed to neutralize \n and \r characters before passing the escaped string to shell execution

Missing control: The escaping regex in shell-quote 1.8.2 did not include line terminator characters in its character class, allowing them to pass through unescaped

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

Fix: Upgrade shell-quote to version 1.9.0, which includes proper escaping for all line terminator characters, combined with adding an npm override to enforce the patched version across all dependencies

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 demonstrates a critical gap in shell-quote 1.8.2's escaping logic that could lead to complete system compromise. The fix—upgrading to version 1.9.0 and enforcing it via npm overrides—is straightforward and essential. However, this vulnerability also highlights a broader principle: escaping alone is not a complete defense. The most secure approach combines:

  1. Using well-maintained security libraries kept up-to-date
  2. Avoiding shell execution entirely when possible
  3. Validating and sanitizing all user input
  4. Automating vulnerability detection in your CI/CD pipeline

Apply this upgrade immediately if you're using shell-quote, and review your codebase for other potential command injection risks. Security is a layered defense—make sure every layer is in place.

References

Frequently Asked Questions

What is command injection via line terminators?

It's a vulnerability where unescaped newline (\n) and carriage return (\r) characters allow attackers to inject additional shell commands that execute on separate lines, bypassing argument escaping logic.

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

Always use a well-maintained library like shell-quote that properly escapes all shell metacharacters including line terminators, and keep dependencies updated to the latest patched versions.

What CWE is this vulnerability?

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

Is using shell-quote alone enough to prevent all command injection?

shell-quote helps when used correctly, but you should also avoid shell execution entirely when possible, use non-shell alternatives like child_process.execFile() with argument arrays, and validate/sanitize user input before passing it to any shell-related function.

Can static analysis detect this vulnerability?

Yes, security scanners like Trivy can detect vulnerable versions of dependencies with known CVEs. However, detecting the actual exploitable code paths requires more sophisticated SAST tools that track data flow from user input to shell-quote calls.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #70

Related Articles

critical

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

A critical command injection vulnerability was discovered in `spider/php/crawler.py` where the `PHPBridge.call()` method passed unvalidated external arguments directly to `subprocess.run()`. An attacker controlling the `spider_path` or `method` parameters could execute arbitrary PHP scripts or inject malicious method names. The fix adds strict input validation — requiring `method` to be a valid Python identifier and `spider_path` to resolve to an existing `.php` file — before any subprocess exec

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `packages/runner/src/main.js` where the `child_process.spawn()` function accepted an unvalidated `argv` array parameter. An attacker could potentially inject malicious arguments to execute arbitrary commands. The fix adds strict type validation for the `argv` array and explicitly disables shell execution to prevent command injection attacks.

critical

How Command Injection Vulnerabilities Happen in Python Subprocess Calls and How to Fix Them

A critical command injection vulnerability was discovered in `src/unused/server/fft.py` where external binaries like `oggenc` and `cocoa_text` were executed with file path parameters that could be manipulated by user input. Although `shell=False` was used, the lack of input validation allowed attackers to potentially trigger processing of arbitrary files or cause denial of service. This fix implements proper path validation to prevent exploitation.

critical

How API Key Exposure and Unsafe Process Spawning Happens in Node.js Scripts and How to Fix It

A critical security vulnerability in the `scripts/close-issues.mjs` file exposed API key patterns in documentation and used unsafe `spawnSync` calls to execute curl commands. The fix replaces dangerous process spawning with native `fetch()` API calls and removes sensitive configuration examples from documentation, eliminating both credential exposure and command injection risks.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A command injection vulnerability was discovered in `scripts/install.js` where user-controllable input was passed to `child_process.execSync()` through string interpolation. This high-severity issue could allow attackers to execute arbitrary shell commands by crafting malicious package file paths. The fix replaces `execSync()` with `execFileSync()`, which bypasses the shell entirely and treats arguments as literal values.

critical

How Arbitrary Code Execution Via Command Injection happens in Node.js and how to fix it

A critical arbitrary code execution flaw in the `shell-quote` npm package (CVE-2026-9277) allowed attackers to break out of shell quoting using unescaped Unicode line terminator characters, turning ordinary command-line arguments into injected shell commands. The fix locks `shell-quote` to the patched `1.8.4` release via a `resolutions` override in `package.json`/`yarn.lock`, closing off a transitive dependency path that could otherwise pull in a vulnerable version.