Back to Blog
critical SEVERITY8 min read

How Command Injection happens in Node.js shell-quote and how to fix it

A critical command injection vulnerability (CVE-2026-9277) was discovered in shell-quote versions prior to 1.8.4, where unescaped line terminators allowed attackers to inject arbitrary shell commands through crafted input strings. The fix pins shell-quote to version 1.9.0 via a `package.json` overrides directive in the FabricExample project, ensuring all transitive dependencies resolve to the patched version. Left unaddressed, this vulnerability could have allowed arbitrary code execution on any

O
By Orbis AppSec
Published August 13, 2026Reviewed August 13, 2026

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability (CWE-78) in the shell-quote npm package (versions before 1.8.4), caused by improper handling of line terminator characters that allows attackers to break out of quoted shell arguments and execute arbitrary commands. In Node.js projects using shell-quote as a direct or transitive dependency, the fix is to pin the package to version 1.8.4 or later — in this case, 1.9.0 — using an npm `overrides` field in `package.json` to force the safe version across the entire dependency tree.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixOverride shell-quote to version 1.9.0 in package.json using npm's `overrides` field
riskArbitrary command execution on the host system processing untrusted input
languageJavaScript / Node.js
root causeshell-quote 1.8.3 failed to escape newline and carriage-return characters, allowing them to terminate a quoted argument and inject new shell commands
vulnerabilityCommand Injection via unescaped line terminators

How Command Injection happens in Node.js shell-quote and how to fix it

Introduction

The FabricExample/package-lock.json file locked a transitive dependency — shell-quote — to version 1.8.3, a version that contains a critical flaw in how it handles line terminator characters. When shell-quote builds a quoted shell string from user-influenced input, it is supposed to neutralize every character that could be interpreted by a shell as a command separator or metacharacter. In version 1.8.3, newline (\n) and carriage-return (\r) characters slipped through without escaping, creating a textbook OS command injection path.

This matters for any Node.js developer who uses shell-quote — directly or as a transitive dependency — to construct shell strings from dynamic input. The package is extremely common in the JavaScript ecosystem; it appears in build tools, CLI helpers, and React Native tooling (hence its presence in a FabricExample project). A single unescaped line terminator is all an attacker needs to turn a quoted argument into two separate shell commands.


The Vulnerability Explained

What shell-quote does

shell-quote is an npm package that provides two utilities:

  • quote(args) — takes an array of strings and returns a single shell-safe string.
  • parse(str) — parses a shell string back into an array of tokens.

The quote() function is the dangerous surface here. Its job is to wrap each argument in quotes and escape characters that shells treat as special. The problem in 1.8.3 is that it did not treat \n (U+000A LINE FEED) or \r (U+000D CARRIAGE RETURN) as special.

The vulnerable pattern

Consider this representative usage:

const quote = require('shell-quote').quote;
const { execSync } = require('child_process');

// filename comes from user input, e.g., a form field or API parameter
const filename = req.query.filename;
const cmd = `cat ${quote([filename])}`;
execSync(cmd, { shell: true });

In shell-quote 1.8.3, if filename is:

report.txt\nrm -rf /tmp/important

Then quote(['report.txt\nrm -rf /tmp/important']) returns something like:

'report.txt
rm -rf /tmp/important'

The shell sees a newline inside what was meant to be a single-quoted string. Depending on the shell and quoting style, this can terminate the first command and execute rm -rf /tmp/important as a separate command — arbitrary code execution achieved with a single newline character.

Why this is critical

The CVSS score is critical because:

  1. No authentication barrier — any endpoint that passes user input through shell-quote into a shell command is exposed.
  2. Trivial to exploit — a newline is a single character, easy to inject via URL encoding (%0A), JSON strings, or form fields.
  3. Full command execution — the attacker controls everything after the injected newline, including reading secrets, exfiltrating data, or establishing persistence.
  4. Transitive exposure — most projects don't use shell-quote directly; they inherit it from build tools, meaning the vulnerable version can be buried several levels deep in the dependency tree.

Attack scenario in FabricExample

In the React Native FabricExample project, shell-quote appears as a transitive dependency of build and bundling tooling. If any build script or Metro bundler plugin passes a user-influenced path or module name through shell-quote into a shell command, an attacker who controls that input (e.g., via a crafted module name in a monorepo or a malicious package) could inject commands that execute during the build process — a classic build-time supply chain attack.


The Fix

What changed

The fix adds a single overrides block to FabricExample/package.json:

Before (package.json — no overrides):

{
  "engines": {
    "node": ">=20"
  }
}

After (package.json — with override):

{
  "engines": {
    "node": ">=20"
  },
  "overrides": {
    "shell-quote": "1.9.0"
  }
}

Why this specific change solves the problem

npm's overrides field (introduced in npm 8.3) forces every package in the dependency tree — regardless of what version they declare as a dependency — to resolve shell-quote to 1.9.0. This is the correct tool for patching transitive vulnerabilities when you cannot immediately update every intermediate package that depends on the vulnerable one.

Version 1.9.0 of shell-quote adds explicit escaping for \n and \r inside the quote() function. The patched output for the attack string above would be something like:

'report.txt\nrm -rf /tmp/important'

...where \n is now represented as a literal backslash-n sequence (or the newline is otherwise neutralized), preventing the shell from interpreting it as a command separator.

Why package.json and not package-lock.json

The lockfile (package-lock.json) is generated — editing it directly would be overwritten on the next npm install. The correct and durable fix is the overrides directive in package.json, which instructs npm to regenerate the lockfile with the forced version. This is why the PR modifies FabricExample/package.json and the resulting package-lock.json update is implied.


Prevention & Best Practices

1. Audit transitive dependencies regularly

Run npm audit and supplement it with a dedicated scanner like Trivy or Snyk. npm audit only checks direct and some transitive dependencies against the npm advisory database; Trivy cross-references against multiple CVE feeds and catches cases npm audit misses.

# Quick audit
npm audit

# Trivy scan of the project directory
trivy fs --scanners vuln .

2. Use overrides (npm) or resolutions (Yarn) proactively

When a transitive dependency has a known vulnerability and the intermediate package hasn't released an update yet, use the package manager's override mechanism:

// npm (package.json)
"overrides": {
  "vulnerable-package": ">=safe-version"
}

// Yarn (package.json)
"resolutions": {
  "vulnerable-package": ">=safe-version"
}

3. Never pass untrusted input to shell-constructing functions

Even with a patched shell-quote, the safest pattern is to avoid shell strings entirely. Use child_process APIs that accept argument arrays:

// ❌ Vulnerable pattern — constructs a shell string
const { execSync } = require('child_process');
execSync(`cat ${quote([userInput])}`, { shell: true });

// ✅ Safe pattern — no shell involved, arguments passed directly
const { execFileSync } = require('child_process');
execFileSync('cat', [userInput]); // userInput cannot inject commands

4. Pin dependency versions in CI

Use npm ci instead of npm install in CI pipelines to enforce the lockfile. Combine this with automated PRs (like the one Orbis AppSec generated) to keep the lockfile current.

5. Reference standards


Key Takeaways

  • shell-quote 1.8.3 does not escape \n or \r — a single newline in user input is enough to inject an arbitrary shell command, making every call to quote() with untrusted data a potential RCE vector.
  • Transitive dependencies are attack surface too — the vulnerability lived in FabricExample/package-lock.json as an indirect dependency, not a package the project authors consciously chose to use.
  • npm overrides is the right tool for forced transitive upgrades — editing package-lock.json directly is fragile; the overrides field in package.json is durable and survives npm install regeneration.
  • execFileSync with an argument array eliminates this entire class of vulnerability — if the codebase can be refactored to avoid shell strings, no amount of malicious input can cause command injection, regardless of what shell-quote does.
  • Trivy caught what npm audit might miss — using multiple scanning tools with different CVE feeds increases the chance of catching newly disclosed vulnerabilities in transitive dependencies before they are exploited.

How Orbis AppSec Detected This

  • Source: User-influenced string data flowing into shell-quote's quote() function — for example, file paths, module names, or CLI arguments derived from external input in build tooling consumed by FabricExample.
  • Sink: The quote() call in shell-quote 1.8.3 that produces an unescaped shell string, subsequently passed to a shell-executing API (execSync, exec, or equivalent) with shell: true.
  • Missing control: shell-quote 1.8.3 lacked escaping for line terminator characters (\n, \r), meaning these characters passed through quote() verbatim and were interpreted by the shell as command separators.
  • CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
  • Fix: Added "overrides": { "shell-quote": "1.9.0" } to FabricExample/package.json, forcing all transitive resolutions of shell-quote to the patched version that correctly escapes line terminators.

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 command injection doesn't require exotic techniques — a single unescaped newline character in a widely-used quoting library is enough to achieve arbitrary code execution. The vulnerability in shell-quote 1.8.3 was subtle: the library correctly escaped most shell metacharacters but overlooked line terminators, a gap that attackers can trivially exploit.

The fix applied here — pinning shell-quote to 1.9.0 via npm overrides in FabricExample/package.json — is minimal, targeted, and durable. It addresses the root cause without touching application logic and without risk of breaking valid inputs. More broadly, this incident highlights the importance of treating transitive dependencies as first-class security concerns: scanning them continuously, using override mechanisms to patch them quickly, and designing application code so that user input never reaches shell-constructing functions in the first place.


References

Frequently Asked Questions

What is command injection via unescaped line terminators?

It's an attack where a malicious string containing newline characters (`\n` or `\r`) is passed to a shell-quoting library. If those characters aren't escaped, they can break out of the current shell argument and cause the shell to interpret the remainder as a new, attacker-controlled command.

How do you prevent command injection in Node.js?

Always keep shell-handling libraries like shell-quote up to date, use npm `overrides` or `resolutions` to force safe versions across transitive dependencies, avoid passing untrusted input to shell commands entirely, and prefer child_process APIs that accept argument arrays instead of shell strings.

What CWE is command injection?

Command injection maps to CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

Is input validation alone enough to prevent command injection?

No. Input validation helps but is not sufficient on its own. The underlying library must also correctly escape all shell metacharacters — including line terminators — before constructing a shell string. A patched library version is required alongside validation.

Can static analysis detect command injection in npm dependencies?

Yes. Tools like Trivy, npm audit, and Semgrep can flag known-vulnerable dependency versions in package-lock.json and package.json files, which is exactly how this CVE-2026-9277 was detected in FabricExample/package-lock.json.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1034

Related Articles

high

How Command Injection happens in Node.js child_process calls and how to fix it

A high-severity command injection vulnerability was discovered in `tools/utils/lang/helpers.ts` where the `prettier()` function passed a user-controllable `fileName` argument directly into a shell command string via `exec()`. By replacing `exec()` with `execFile()` and passing arguments as an array, the fix eliminates shell interpolation entirely, preventing attackers from injecting arbitrary shell commands through malicious filenames.

high

How Command Injection Happens in Node.js child_process and How to Fix It

A high-severity command injection vulnerability was discovered in `scripts/common.js` where the `exec()` function used `execSync()` with unsanitized input, allowing potential command injection attacks. The fix replaces `execSync()` with `execFileSync()` and separates command arguments into an array, preventing shell metacharacter interpretation. This defensive hardening removes an exploit primitive that could be chained with other weaknesses by automated attack tools.

high

How Command Injection Happens in Node.js Child Process Calls and How to Fix It

A Node.js library was vulnerable to command injection through unsafe use of `execSync()` with shell string interpolation in the `index.js` file. By switching to `execFileSync()` with argument arrays, the fix eliminates the ability for attackers to inject shell metacharacters through file paths. This change demonstrates a critical security hardening pattern for any Node.js code that spawns child processes.

high

How Command Injection via child_process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `bin/init.mjs` where the `shallowClone` function passed a user-controllable `ref` parameter directly to `execSync` shell commands. This could allow attackers to execute arbitrary system commands by crafting malicious git reference names. The fix implements strict input validation and replaces `execSync` with `execFileSync` to eliminate shell interpretation entirely.

critical

How Command Injection happens in Python subprocess and how to fix it

A critical command injection vulnerability was discovered in the `open_directory` method of `src/jm_view_server/app.py`, where user-controlled path input was passed directly into a shell command via `subprocess.Popen`. By switching from string-based shell execution to a list-based argument format, the fix eliminates the ability for attackers to inject malicious shell commands through crafted directory paths.

critical

How Sensitive Data Exposure in Error Logging happens in TypeScript/Deno and how to fix it

A critical vulnerability in Supabase Edge Functions allowed sensitive authentication errors and API credentials to leak through verbose error logging. The `cancel-subscription/index.ts` function logged full error objects to the console, potentially exposing Paddle API keys and auth tokens in deployment logs. The fix sanitizes all error messages to log only safe error text while preserving debugging capability.