How Command Injection Happens in Node.js shell-quote and How to Fix It
At a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-9277 |
| Severity | Critical |
| Package | shell-quote (npm) |
| Affected version | 1.8.3 and earlier |
| Fixed version | 1.8.4 |
| Root cause | Unescaped line terminators enabling shell command injection |
| CWE | CWE-78: OS Command Injection |
Introduction
The package-lock.json in this project locked shell-quote to version 1.8.3 — a version that contains a critical flaw in its core purpose: safely quoting shell arguments. When a library whose entire job is to prevent command injection is itself vulnerable to command injection, the consequences ripple through every application that trusts it to sanitize user input before passing strings to the shell.
Trivy's scanner flagged rule CVE-2026-9277 against this exact version, identifying that the library failed to escape line terminator characters (\n, \r, and similar Unicode line endings). This is not a theoretical edge case. Any code path in your application that takes user-influenced input, passes it through shell-quote, and then hands the result to a shell executor is potentially exploitable — regardless of how carefully the rest of your code is written.
The Vulnerability Explained
What shell-quote Does (and Why It Matters)
shell-quote is an npm package used to safely construct shell command strings from arrays of arguments. A typical usage looks like this:
const quote = require('shell-quote').quote;
const userInput = req.query.filename;
const cmd = `cat ${quote([userInput])}`;
exec(cmd, callback);
The intention is that quote() will escape any dangerous characters in userInput so that the resulting cmd is safe to pass to a shell. For most characters — spaces, semicolons, backticks, dollar signs — version 1.8.3 does this correctly.
The Flaw: Unescaped Line Terminators
The vulnerability in 1.8.3 is that line terminator characters are not treated as special. Shell interpreters (bash, sh, zsh) treat a newline (\n) as a command separator — functionally equivalent to a semicolon. If an attacker can inject a literal newline character into input that is subsequently passed through shell-quote, the quoted output will contain an unescaped newline, and the shell will interpret everything after it as a new, separate command.
Consider this attack input:
innocent_file.txt\nrm -rf /tmp/important
In shell-quote 1.8.3, this would be quoted in a way that preserves the literal newline, producing something like:
cat 'innocent_file.txt'
rm -rf /tmp/important
The shell sees two commands and executes both. The attacker has achieved arbitrary command execution with the privileges of the Node.js process — without ever breaking out of a quoted string in the traditional sense.
Why This Is Critical
The severity is critical (not just high) because:
- No authentication bypass required — any input vector that reaches the quoting call is sufficient.
- The fix location is a trusted library — developers who use
shell-quotedo so specifically to avoid writing their own escaping logic. A flaw here undermines the entire security model. - Line terminators are easy to inject — HTTP query parameters, form fields, JSON body values, file names from uploads — all of these can contain
\nor\r\nunless explicitly stripped upstream.
The Fix
What Changed in the Dependency
The fix required two coordinated changes: updating the resolved version of shell-quote in package-lock.json, and adding an explicit overrides entry in package.json to ensure the patched version is used regardless of what other packages in the dependency tree might request.
package-lock.json — Version and Integrity Update
"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.8.4",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz",
+ "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
The integrity hash change is significant — it is a cryptographic guarantee (SHA-512) that the downloaded package matches the expected content. Changing this hash confirms that the installed artifact is genuinely the new 1.8.4 release and not a re-tagged version of 1.8.3.
package.json — Pinning via overrides
"overrides": {
- "picomatch": "^4.0.3"
+ "picomatch": "^4.0.3",
+ "shell-quote": "1.8.4"
}
This is the more important of the two changes from a security maintenance perspective. shell-quote may appear as a transitive dependency — pulled in by other packages rather than directly by the application. Without the overrides entry, npm's dependency resolution could install 1.8.3 for a nested package even after package-lock.json has been updated at the top level. The overrides field forces npm to resolve all instances of shell-quote in the dependency tree to 1.8.4, closing the vulnerability regardless of where in the tree it appears.
How 1.8.4 Fixes the Escaping
Version 1.8.4 adds line terminator characters to the set of characters that trigger quoting or escaping in the output. Specifically, \n, \r, and potentially other Unicode line separators are now treated as unsafe characters that must be escaped before they appear in a quoted shell argument. This means the attack payload described earlier — innocent_file.txt\nrm -rf /tmp/important — would produce output where the newline is escaped and the shell sees it as part of the filename argument, not as a command separator.
Prevention & Best Practices
1. Keep Shell-Handling Dependencies Pinned and Audited
Any package that touches shell construction is in your application's security critical path. These dependencies deserve:
- Explicit version pinning (not
^or~ranges alone) - Regular
npm auditruns in CI - Automated vulnerability scanning (Trivy, Snyk, or similar) on every pull request
2. Prefer Array-Based Process APIs
Where possible, avoid constructing shell strings altogether. Node.js's child_process.execFile() and child_process.spawn() (without shell: true) accept argument arrays and bypass the shell entirely:
// Vulnerable pattern — passes through shell
const { exec } = require('child_process');
exec(`cat ${quote([userInput])}`, callback);
// Safer pattern — no shell involved
const { execFile } = require('child_process');
execFile('cat', [userInput], callback);
When you use execFile or spawn with an arguments array, line terminators in the input are passed as literal data to the process — the shell never sees them.
3. Validate and Sanitize Input at the Boundary
Even with a patched shell-quote, consider stripping or rejecting line terminator characters at the point where user input enters your system:
function sanitizeForShell(input) {
// Reject inputs containing line terminators before they reach shell construction
if (/[\n\r\u2028\u2029]/.test(input)) {
throw new Error('Invalid input: line terminators not permitted');
}
return input;
}
This is defense in depth — the library fix is the primary control, but input validation at the boundary provides a second layer.
4. Use npm overrides for Transitive Dependency Security
As demonstrated in this fix, npm's overrides field (introduced in npm 8.3) is a powerful tool for enforcing minimum safe versions across the entire dependency tree:
"overrides": {
"shell-quote": "1.8.4"
}
Make this part of your standard response playbook whenever a transitive dependency is flagged with a critical CVE.
5. Reference 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 Injection Prevention Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Injection_Prevention_Cheat_Sheet.html
Key Takeaways
shell-quote1.8.3 does not escape\nand\r— any application passing user input through this version and into a shell executor is vulnerable to command injection, regardless of other safeguards.- Upgrading the direct dependency is not always enough — the
overridesentry inpackage.jsonis required to patch transitive instances ofshell-quotethat other packages in your tree may pull in independently. - The
integrityhash inpackage-lock.jsonis a security control — the change from the1.8.3SHA-512 to the1.8.4SHA-512 provides cryptographic assurance that the correct artifact is installed. - Line terminators are valid command separators in most shells — escaping libraries must treat
\n,\r, and Unicode line separators (U+2028, U+2029) as dangerous characters, not just the "classic" injection characters like;,|, and backticks. child_process.execFile()with an argument array eliminates this entire class of vulnerability for new code — prefer it overexec()with shell string construction wherever feasible.
How Orbis AppSec Detected This
- Source: User-influenced input flowing into shell command construction via the
shell-quotequoting function - Sink: The
shell-quotequote function innode_modules/shell-quote, called before passing constructed strings to shell executors such aschild_process.exec() - Missing control:
shell-quote1.8.3 did not include line terminator characters (\n,\r, U+2028, U+2029) in its set of characters requiring escaping, allowing them to pass through unmodified into shell command strings - CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- Fix: Upgraded
shell-quotefrom1.8.3to1.8.4inpackage-lock.jsonand added a"shell-quote": "1.8.4"entry to theoverridesblock inpackage.jsonto enforce 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 sharp reminder that security libraries are not immune to security vulnerabilities. shell-quote exists specifically to make shell command construction safe — yet a single missing character class in its escaping logic created a critical command injection vector. The fix is straightforward: upgrade to 1.8.4 and use npm overrides to ensure the patch applies everywhere in your dependency tree. But the broader lesson is architectural: the safest code is code that never reaches the shell in the first place. Where you can use execFile with argument arrays instead of exec with shell strings, do so. Layer your defenses — patched libraries, input validation at boundaries, and shell-free process APIs — so that no single library flaw can become a system compromise.