Back to Blog
high SEVERITY7 min read

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.

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

Answer Summary

CVE-2026-9277 is a command injection vulnerability in the Node.js `shell-quote` package (CWE-78: Improper Neutralization of Special Elements used in an OS Command) caused by inadequate escaping of line terminators in shell arguments. When untrusted input containing newlines or carriage returns was processed, attackers could inject arbitrary shell commands. The fix upgrades `shell-quote` from version 1.8.3 to 1.9.0, which properly escapes line terminators to prevent command injection, ensuring that user-controlled input cannot break out of quoted arguments.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixUpgrade shell-quote to 1.9.0 which implements proper line terminator escaping
riskArbitrary code execution when processing untrusted shell arguments
languageJavaScript/Node.js
root causeshell-quote 1.8.3 failed to escape newline and carriage return characters in shell arguments
vulnerabilityCommand Injection via Unescaped Line Terminators

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

Introduction

In the docs-site repository, a critical command injection vulnerability (CVE-2026-9277) was discovered lurking in the dependency tree through the shell-quote package. The vulnerability wasn't in custom code—it was in a transitive dependency that handles shell argument escaping, a task that seems simple but has profound security implications.

The issue: shell-quote version 1.8.3 failed to properly escape line terminator characters (newlines and carriage returns) in shell arguments. This seemingly minor oversight created a dangerous window for attackers to inject arbitrary shell commands by embedding these special characters in what should have been safely quoted arguments.

For any Node.js application that uses shell-quote to safely construct shell commands from user-controlled input, this vulnerability represented a direct path to remote code execution. The fix was straightforward but critical: upgrade to version 1.9.0, which implements proper escaping of line terminators.

The Vulnerability Explained

What Made This Dangerous?

The shell-quote package is designed to solve a common problem in Node.js: when you need to pass user-controlled strings as arguments to shell commands, you must properly escape special characters. Without proper escaping, an attacker can inject shell metacharacters (like ;, |, &, $()) to execute arbitrary commands.

However, shell-quote 1.8.3 had a blind spot: it didn't account for line terminator characters (newline \n and carriage return \r). Here's why this matters:

In most shell contexts, when a quoted string contains a newline, the shell interprets it as the end of the current command and the beginning of a new one. An attacker could craft input like:

user input: "safe_arg\nmalicious_command"

When processed by shell-quote 1.8.3 and then passed to a shell, it would be treated as:

safe_arg
malicious_command

Instead of a single safe argument, the attacker has successfully injected a second command that executes with the same privileges as the original process.

The Attack Scenario

Imagine a Node.js application in docs-site that uses shell-quote to safely construct a command for documentation generation:

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

// User-provided documentation filename
const userFilename = req.query.filename;

// Attempt to safely quote the filename
const quotedFilename = shellQuote.quote([userFilename]);

// Execute a documentation processing command
const command = `process-docs --file ${quotedFilename}`;
execSync(command);

With shell-quote 1.8.3, if an attacker provides:

filename=report.md\nrm -rf /important/data

The resulting command becomes:

process-docs --file report.md
rm -rf /important/data

Both commands execute. The attacker has achieved arbitrary code execution.

Why Line Terminators Were Missed

The vulnerability existed because earlier versions of shell-quote focused on escaping traditional shell metacharacters (;, |, &, $, backticks, etc.) but didn't consider that line terminators could also break the shell context. This is a classic example of incomplete input sanitization—addressing the most obvious attack vectors while missing edge cases.

The scanner (Trivy) flagged this as present in the dependency tree with the assessment "not confirmed reachable," meaning the vulnerability existed in the dependency graph but the actual code path might not have been exercised. However, the principle of defense in depth dictates that you should patch known vulnerabilities regardless—transitive dependencies can be called in unexpected ways, and future code changes might activate the vulnerable path.

The Fix

What Changed?

The fix involved upgrading shell-quote from version 1.8.3 to 1.9.0 and adding an explicit override in package.json to ensure this version is used throughout the dependency tree.

In docs-site/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==",
+      "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==",

In docs-site/package.json:

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

How This Solves the Problem

Version 1.9.0 of shell-quote implements proper escaping of line terminator characters. When the package encounters \n or \r in user input, it now escapes them appropriately so they're treated as literal characters within the quoted string, not as command separators.

With the patched version, the attack scenario from earlier is neutralized:

// With shell-quote 1.9.0
const userFilename = "report.md\nrm -rf /important/data";
const quotedFilename = shellQuote.quote([userFilename]);
// Result: properly escapes the newline, preventing command injection
// The entire string is treated as a single filename argument

The overrides field in package.json is particularly important. It ensures that even if another dependency in the tree specifies an older version of shell-quote, npm will use 1.9.0 instead. This prevents the vulnerable version from being installed through transitive dependency resolution.

Why Both Files Were Changed

  • package-lock.json: Records the exact version and integrity hash of the installed package, ensuring reproducible builds
  • package.json: Adds an explicit override to guarantee that 1.9.0 is used across the entire dependency tree, even if other packages request different versions

This two-pronged approach prevents dependency confusion and ensures the fix persists across team members' installations and CI/CD pipelines.

Prevention & Best Practices

1. Keep Dependencies Updated

Regularly update security-sensitive packages like shell-quote. Enable automated dependency updates through tools like Dependabot or Renovate, which can automatically create PRs for security patches.

2. Use Parameterized APIs

Whenever possible, avoid constructing shell commands from strings. Use APIs that treat arguments as data, not code:

// ❌ Avoid: Command as string
execSync(`process-docs --file ${userFilename}`);

// ✅ Better: Arguments as array (no shell parsing)
execFileSync('process-docs', ['--file', userFilename]);

3. Validate and Sanitize Input

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

// Validate filename format before processing
if (!/^[a-zA-Z0-9_\-\.]+$/.test(userFilename)) {
  throw new Error('Invalid filename format');
}

4. Use Security Scanners

Integrate tools like Trivy, npm audit, and Snyk into your CI/CD pipeline to automatically detect vulnerable dependencies:

# Run Trivy to scan for known vulnerabilities
trivy fs docs-site/

# Run npm audit
npm audit --audit-level=moderate

5. Understand CWE-78

Familiarize yourself with CWE-78: Improper Neutralization of Special Elements used in an OS Command. Many command injection vulnerabilities follow similar patterns.

6. Review Transitive Dependencies

Use npm ls to understand your full dependency tree:

npm ls shell-quote

Know what versions of critical security packages are installed, even indirectly.

Key Takeaways

  • Line terminators are special: Command injection isn't limited to traditional shell metacharacters like ; and |. Newlines and carriage returns can be equally dangerous when not properly escaped.

  • shell-quote 1.8.3 was incomplete: The package addressed common injection vectors but missed line terminator escaping, demonstrating that security libraries require continuous improvement as attack techniques evolve.

  • Transitive dependencies matter: Even though shell-quote was a transitive dependency in docs-site, the vulnerability posed a real risk. Scanning and patching all layers of the dependency tree is essential.

  • Defense in depth requires override mechanisms: The overrides field in package.json is a critical tool for ensuring security patches propagate through complex dependency trees where multiple packages might specify conflicting versions.

  • Automated detection is reliable for known CVEs: Trivy and similar scanners can reliably flag vulnerable versions of well-known packages, but human judgment is still required to assess actual exploitability and prioritize fixes.

How Orbis AppSec Detected This

Source: Transitive dependency declaration in docs-site/package.json and docs-site/package-lock.json (shell-quote 1.8.3)

Sink: Any code path that uses shell-quote to escape arguments for shell execution (e.g., in command construction utilities or subprocess wrappers)

Missing control: Absence of line terminator escaping in shell-quote 1.8.3's quoting logic; no explicit version override to enforce patched versions across the dependency tree

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

Fix: Upgrade shell-quote from 1.8.3 to 1.9.0 which properly escapes line terminators, and add an explicit npm override to ensure the patched version is used throughout the 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 demonstrates a critical principle in secure coding: security libraries must be comprehensive in their protection. A single missed edge case—in this case, line terminator characters—can completely undermine the security guarantees a library is supposed to provide.

For Node.js developers, this vulnerability reinforces several important practices:

  1. Trust but verify: Even widely-used security libraries like shell-quote can have gaps. Keep them updated and monitor security advisories.

  2. Defense in depth: Don't rely solely on shell-quote for security. Use parameterized APIs, validate input, and avoid shell=true when possible.

  3. Dependency management is security work: Maintaining a secure application means actively managing transitive dependencies, not just direct ones. Tools like npm overrides are your allies.

  4. Automation catches what humans miss: Security scanners like Trivy can reliably identify known vulnerable versions. Integrate them into your CI/CD pipeline and act on their findings promptly.

By understanding how this vulnerability worked and why the fix was necessary, you're better equipped to recognize similar issues in your own code and dependencies—and to build systems that are resilient against command injection attacks.


References

Frequently Asked Questions

What is command injection via line terminators?

It's a vulnerability where newline or carriage return characters in shell arguments aren't properly escaped, allowing attackers to inject new shell commands after breaking out of the current argument context.

How do you prevent command injection in Node.js?

Always use libraries like `shell-quote` that properly escape special characters, avoid `shell: true` in child processes, validate and sanitize all user input, and use parameterized APIs that treat arguments as data, not code.

What CWE is this vulnerability?

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

Is using shell-quote enough to prevent command injection?

Only if you're using a patched version that properly escapes all special characters including line terminators. Version 1.8.3 was not sufficient; 1.9.0 and later are required.

Can static analysis detect command injection via line terminators?

Yes, security scanners like Trivy can detect vulnerable versions of shell-quote in dependency trees. However, detecting whether line terminators are actually exploitable requires understanding the full data flow context.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1279

Related Articles

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

How Command Injection happens in Node.js and how to fix it

A semgrep scan flagged `scripts/postinstall.js` for calling `child_process.execSync` in a way that could become a command injection primitive if the script's execution context ever changed. The fix hardens the script by guarding its side effects behind a `require.main === module` check, introducing the safer `execFileSync` API, and adding automated tests to lock in the safe behavior.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

How Command Injection happens in Node.js and how to fix it

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How Shell Injection Happens in GitHub Actions and How to Fix It

A high-severity shell injection vulnerability was discovered in `action.yml` where direct variable interpolation with GitHub context data in `run:` steps could allow attackers to inject arbitrary code into the runner. The fix uses environment variables with proper quoting to safely separate untrusted input from shell execution, eliminating the exploit primitive while preserving legitimate functionality.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.