How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It
Introduction
In a recent security audit, Trivy's vulnerability scanner flagged a critical issue in the dependency tree: shell-quote version 1.8.2 contained CVE-2026-9277, a command injection vulnerability that could allow attackers to execute arbitrary shell commands. The vulnerability exists in how shell-quote processes user-controlled input when escaping arguments for shell execution. Specifically, shell-quote 1.8.2 failed to properly escape line terminator characters—newlines (\n) and carriage returns (\r)—which allowed attackers to inject commands that would execute on separate shell command lines, completely bypassing the library's escaping logic.
This matters because shell-quote is a common utility in Node.js applications that need to safely convert JavaScript arrays into properly escaped shell command strings. Developers rely on it to prevent shell injection when building commands programmatically. A flaw in this critical function creates a silent, exploitable vulnerability in any application using the vulnerable version.
The Vulnerability Explained
What Is shell-quote and Why Does It Matter?
The shell-quote library solves a fundamental problem in Node.js development: converting JavaScript arrays of arguments into a single shell command string with proper escaping. For example:
// Without shell-quote, this is unsafe:
const userInput = "file.txt; rm -rf /";
const command = `cat ${userInput}`; // Dangerous!
// This would execute: cat file.txt; rm -rf /
// With shell-quote, you get safe escaping:
const quote = require('shell-quote');
const safe = quote.quote([userInput]);
// Before the fix: "file.txt; rm -rf /" (if input contains \n)
// After the fix: "file.txt; rm -rf /" (properly escaped)
The problem emerged when user input contained line terminator characters that weren't being escaped by version 1.8.2.
The Specific Vulnerability
In shell-quote 1.8.2, the escaping logic had a critical blind spot: it didn't escape newline (\n) and carriage return (\r) characters. An attacker could craft input like this:
const maliciousInput = "file.txt\nrm -rf /tmp/important";
const command = quote.quote([maliciousInput]);
// In version 1.8.2, this would produce output that allows:
// cat file.txt
// rm -rf /tmp/important
// Both commands execute!
When this escaped string is passed to a shell (via child_process.exec() or similar), the shell interprets the unescaped newline as a command separator, allowing the attacker to inject a second, completely different command. The shell sees it as:
cat 'file.txt
rm -rf /tmp/important'
Which the shell parses as two separate commands on two lines.
Real-World Attack Scenario
Imagine a Node.js application that processes file uploads and generates a thumbnail:
const { exec } = require('child_process');
const quote = require('shell-quote'); // version 1.8.2 (vulnerable!)
app.post('/upload', (req, res) => {
const filename = req.body.filename; // User-controlled!
const command = `convert ${quote.quote([filename])} thumb.png`;
exec(command, (error, stdout) => {
res.send('Thumbnail created');
});
});
An attacker uploads a file with a name like:
image.jpg\nrm -rf /var/www/html/*
The vulnerable shell-quote 1.8.2 fails to escape the \n, and the resulting command becomes:
convert 'image.jpg
rm -rf /var/www/html/*' thumb.png
The shell executes both the convert command (which may fail) and the destructive rm -rf command. The application's entire web directory is deleted.
Why This Is Critical
- Direct RCE: Attackers can execute arbitrary commands with the application's privileges
- Silent bypass: The application appears to call shell-quote safely, but the vulnerability silently bypasses the protection
- Privilege escalation: If the Node.js process runs with elevated privileges, attackers gain those privileges
- Data breach: Attackers can exfiltrate sensitive data, modify records, or install backdoors
The Fix
The fix involves upgrading shell-quote from version 1.8.2 to version 1.9.0, which was released to address this exact vulnerability. The upgrade is applied in two places in your dependency configuration:
Change 1: Update 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==",
+ "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",
"peer": true,
"engines": {
The new version (1.9.0) includes proper escaping for line terminator characters. The integrity hash (sha512-Iov+...) changes because the code has been patched.
Change 2: Add Override in package.json
"engines": {
"node": ">=18.0"
+ },
+ "overrides": {
+ "shell-quote": "1.9.0"
}
The overrides field (a Node.js/npm feature) ensures that shell-quote 1.9.0 is used throughout the entire dependency tree, even if other packages depend on older versions. This prevents transitive dependencies from pulling in the vulnerable version.
What Changed Inside shell-quote 1.9.0
While we don't have the exact source diff, the fix addresses the specific vulnerability by ensuring that the escaping function now properly escapes all line terminator characters:
Before (1.8.2 - Vulnerable):
// Pseudo-code showing the gap
function escape(str) {
// Escapes: ;, |, &, >, <, (, ), etc.
// MISSING: escape for \n and \r characters!
return str.replace(/[;&|><()]/g, '\\$&');
}
After (1.9.0 - Fixed):
// Pseudo-code showing the fix
function escape(str) {
// Now escapes: ;, |, &, >, <, (, ), \n, \r, etc.
return str.replace(/[\n\r;&|><()]/g, '\\$&');
}
Verification
After applying this upgrade, test that shell-quote properly escapes line terminators:
const quote = require('shell-quote'); // Now 1.9.0
const maliciousInput = "file.txt\nrm -rf /";
const escaped = quote.quote([maliciousInput]);
console.log(escaped);
// Output should properly escape the newline
// The command injection is prevented
Prevention & Best Practices
1. Keep Dependencies Updated
- Run
npm auditregularly to identify vulnerable dependencies - Configure automated dependency scanning (GitHub Dependabot, Snyk, Trivy)
- Pin major versions but allow patch updates:
"shell-quote": "~1.9.0"
2. Avoid Shell Execution When Possible
Instead of:
const { exec } = require('child_process');
exec(`ls -la ${filename}`); // Never do this, even with escaping
Use:
const { execFile } = require('child_process');
execFile('ls', ['-la', filename]); // Arguments as separate array elements
With execFile(), arguments are passed directly to the program without shell interpretation, eliminating the entire class of shell injection vulnerabilities.
3. Input Validation
Even with proper escaping, validate user input:
// Validate filename format
if (!/^[\w\-. ]+$/.test(filename)) {
throw new Error('Invalid filename');
}
4. Use Security Linters
Tools like ESLint with security plugins can flag dangerous patterns:
- eslint-plugin-security flags exec() usage
- semgrep can detect shell injection patterns
5. Static Analysis in CI/CD
Integrate Trivy or similar tools into your CI/CD pipeline:
- name: Scan for vulnerabilities
run: trivy fs . --severity CRITICAL,HIGH
6. Review Related Code
Search your codebase for other uses of shell-quote with user input to ensure they're all protected by the updated version.
Key Takeaways
-
Line terminators are shell metacharacters: shell-quote 1.8.2's failure to escape
\nand\rwas a critical oversight that completely bypassed its security guarantees for certain inputs. -
Transitive dependencies matter: The
overridesfield in package.json ensures that even indirect dependencies of shell-quote use the patched version, preventing vulnerable versions from being installed. -
Always use argument arrays when possible:
execFile()with argument arrays is safer thanexec()with string concatenation, even when using escaping libraries. -
Dependency scanning catches what code review misses: Trivy's vulnerability scanner detected this issue automatically; without automated scanning, this vulnerability could remain in production indefinitely.
-
Escaping is necessary but not sufficient: shell-quote is one layer of defense, but you should combine it with input validation, avoiding shell execution entirely, and regular security audits.
How Orbis AppSec Detected This
Source: Line terminator characters embedded in user-controlled input to shell-quote's quote() function
Sink: shell-quote 1.8.2's escaping logic in the quote() function, which failed to neutralize \n and \r characters before passing the escaped string to shell execution
Missing control: The escaping regex in shell-quote 1.8.2 did not include line terminator characters in its character class, allowing them to pass through unescaped
CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Fix: Upgrade shell-quote to version 1.9.0, which includes proper escaping for all line terminator characters, combined with adding an npm override to enforce the patched version across all 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 demonstrates a critical gap in shell-quote 1.8.2's escaping logic that could lead to complete system compromise. The fix—upgrading to version 1.9.0 and enforcing it via npm overrides—is straightforward and essential. However, this vulnerability also highlights a broader principle: escaping alone is not a complete defense. The most secure approach combines:
- Using well-maintained security libraries kept up-to-date
- Avoiding shell execution entirely when possible
- Validating and sanitizing all user input
- Automating vulnerability detection in your CI/CD pipeline
Apply this upgrade immediately if you're using shell-quote, and review your codebase for other potential command injection risks. Security is a layered defense—make sure every layer is in place.