Back to Blog
critical SEVERITY4 min read

How Arbitrary Code Execution Via Command Injection happens in Node.js and how to fix it

A critical arbitrary code execution flaw in the `shell-quote` npm package (CVE-2026-9277) allowed attackers to break out of shell quoting using unescaped Unicode line terminator characters, turning ordinary command-line arguments into injected shell commands. The fix locks `shell-quote` to the patched `1.8.4` release via a `resolutions` override in `package.json`/`yarn.lock`, closing off a transitive dependency path that could otherwise pull in a vulnerable version.

O
By Orbis AppSec
•Technically reviewed by Anupam Mediratta•Published August 31, 2026•Reviewed August 31, 2026

Answer Summary

CVE-2026-9277 is a critical command injection vulnerability in the `shell-quote` npm package (CWE-78) caused by its `quote()`/`parse()` functions failing to escape Unicode line separator (U+2028) and paragraph separator (U+2029) characters. Because some JavaScript and shell environments treat these characters as line terminators, attacker-controlled strings containing them could break out of quoting and execute arbitrary shell commands. The fix is to pin `shell-quote` to the patched version `1.8.4` using a `resolutions` entry in `package.json`, forcing every transitive dependency to resolve to the safe release.

Vulnerability at a Glance

cweCWE-78 (Improper Neutralization of Special Elements used in an OS Command)
fixPin `shell-quote` to the patched release `1.8.4` via a `resolutions` override in `package.json`/`yarn.lock`
riskAttacker-controlled input passed through shell-quote's quoting functions can escape quoting and inject arbitrary shell commands
languageJavaScript / Node.js
root causeshell-quote's `quote()` and `parse()` did not escape U+2028/U+2029 line terminator characters before building shell command strings
vulnerabilityArbitrary code execution via command injection (unescaped line terminators)

Introduction

The yarn.lock file in a Node.js project is more than a housekeeping artifact — it's the definitive record of exactly which versions of every dependency, direct and transitive, get installed. When one of those pinned versions has a critical flaw, the lockfile becomes the attack surface. That's exactly what happened here: shell-quote, a small utility used to safely quote and parse shell command strings, shipped a version with a critical arbitrary code execution vulnerability tracked as CVE-2026-9277.

The bug lived in shell-quote's core escaping functions, quote() and parse(). These functions are supposed to take a string and wrap it so it can be safely embedded in a shell command — the whole point of the library is to prevent shell metacharacters from being interpreted unexpectedly. But the escaping logic missed two specific Unicode characters: the line separator (U+2028) and paragraph separator (U+2029). In several JavaScript engines and shell environments, these characters are treated as line terminators — effectively letting an attacker "start a new line" inside what was supposed to be a single, safely-quoted token. If that new line contained additional shell syntax, it could be executed as a separate command.

For any application that uses shell-quote to build command-line invocations from user-influenced strings — filenames, search terms, CLI arguments passed through from a web form — this is a direct path from untrusted input to remote code execution on the host running the Node.js process.

The Vulnerability Explained

At a conceptual level, shell-quote's job is to turn something like:

const { quote } = require('shell-quote');
quote(['echo', userInput]);

into a string that's safe to hand off to child_process.exec() or a real shell, no matter what userInput contains. The escaping routine is supposed to wrap dangerous characters — spaces, quotes, $, backticks, semicolons — so they're treated as literal text rather than shell syntax.

The vulnerable versions of shell-quote did this correctly for the "classic" set of shell metacharacters, but not for U+2028 (LINE SEPARATOR) and U+2029 (PARAGRAPH SEPARATOR). Because these characters:

  • Were not part of the escaping allowlist/denylist logic in quote(), and
  • Are interpreted as line terminators by some downstream shells and JavaScript string handling,

an attacker who could get one of these characters into the input string could effectively terminate the "quoted" segment early and inject a new shell statement after it — all while shell-quote believed it had produced a fully-escaped, safe string.

Example attack scenario: imagine a build tool or CI script that shells out using shell-quote to safely construct a command from a user-supplied branch name or file path, e.g.:

const cmd = quote(['git', 'checkout', branchName]);
exec(cmd);

If branchName is attacker-controlled (via a webhook payload, an uploaded file, or a form field) and contains a U+2028 character followed by ; rm -rf / #, the unpatched shell-quote would fail to neutralize that sequence. The resulting string, once handed to a shell, could execute the injected command — turning a routine git checkout into arbitrary code execution on the CI runner or application server.

Because shell-quote is often a transitive dependency (pulled in by build tooling, linters, or dev dependencies rather than referenced directly), many teams don't even realize it's part of their dependency graph until a scanner like Trivy flags it against the yarn.lock.

The Fix

The fix doesn't touch application code at all — it happens entirely at the dependency-resolution layer, which is the right place to fix a vulnerable transitive package. Looking at the actual diff for this maintenance pass:

Before (package.json):

"resolutions": {
  "@babel/runtime": "^7.26.10",
  "libsodium-wrappers-sumo": "0.7.15",
  "shell-quote": "1.8.4"
}

After (package.json):

"resolutions": {
  "@babel/runtime": "^7.26.10",
  "libsodium-wrappers-sumo": "0.7.15",
  "shell-quote": "1.8.4",
  "websocket-driver": "0.7.5"
}

The resolutions field is a Yarn feature that forces every package in the dependency tree — no matter how deeply nested, no matter what version range a parent package requests — to resolve to a single, specified version. The entry "shell-quote": "1.8.4" is what closes CVE-2026-9277: 1.8.4 is the patched release where quote() and parse() correctly escape U+2028 and U+2029 alongside the rest of the shell metacharacter set. Regardless of how many different tools in node_modules depend on shell-quote and regardless of what (possibly older, vulnerable) version range they specify, Yarn is instructed to always install 1.8.4.

In this same commit, the team extended the pattern to a second vulnerable dependency, websocket-driver, adding "websocket-driver": "0.7.5" to the same resolutions block to remediate a separate issue (CVE-2026-54466). The corresponding yarn.lock entry shows the effect of that override:

-websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
-  version "0.7.4"
-  resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#..."
-  integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==
+websocket-driver@0.7.5, websocket-driver@>=0.5.1, websocket-driver@^0.7.4:
+  version "0.7.5"
+  resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.5.tgz#..."
+  integrity sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==

The shell-quote entry in yarn.lock does

Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #751

Related Articles

critical

ffmpeg.Probe() Protocol Abuse in the GetSnapshot Thumbnailer

The local storage driver's thumbnail helpers — `GetSnapshot()` and `resizeImageToBufferWithFFmpegGo()` — handed user-controlled paths to `ffmpeg.Input()` and `ffmpeg.Probe()` with no restriction on which FFmpeg protocols those processes could open. A file that passed the existing `sanitizeFilePath` check (absolute, cleaned, regular file) could still be an HLS playlist or concat script pointing at `http://`, `subfile:` or `file:` targets, turning thumbnail generation into SSRF and arbitrary file

critical

webapp-testing Server Script: Shell Injection via Unquoted Command

A critical command injection vulnerability in a web server testing utility allowed attackers to execute arbitrary shell commands by injecting metacharacters into command-line arguments. The fix removes shell interpretation and safely tokenizes user-supplied commands using `shlex.split()`, while preserving support for legitimate directory-change workflows.

high

THiNX Device Platform: Command Injection via source.owner in Git Fetch

The THiNX device management platform fixed a critical vulnerability where the `source.owner` parameter flowed directly into shell commands during git repository operations. An attacker with source configuration access could inject arbitrary shell commands by providing malicious owner values containing metacharacters like `; curl`.

critical

backtest.js CLI args: Unvalidated --days and --start Options

The backtest.js command-line tool accepted --days and --start arguments without validation, creating a command injection vector when the script is invoked by upstream processes with untrusted input. The fix adds strict input validation: --days must be a positive integer ≤ 3650, and --start must match the YYYY-MM-DD date format.

critical

Tauri type_text() Command Injection via Control Characters in

The type_text() system command handler in Tauri accepted arbitrary text up to 2000 UTF-16 characters without validating content, allowing injection of control characters that SendInput's Unicode path interprets as real key presses. An attacker could inject newline, escape, or tab characters to trigger actions in the focused window beyond mere text typing.

high

runStreaming() Command Injection: Defense-in-Depth for Electron Child

An Electron application's `runStreaming()` utility accepted a command string and argument array without validating either, creating a latent command injection vector. The fix adds strict type checking and a whitelist regex that rejects shell metacharacters, bounding the failure mode even if caller input becomes attacker-influenced.