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
Published August 31, 2026Reviewed 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

high

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

A high-severity command injection vulnerability was discovered in `server.js` where user-controlled file paths were passed directly to shell commands via `exec()`. By migrating from `exec()` to `execFile()` and using argument arrays instead of string concatenation, the fix eliminates the attack surface while preserving the intended trash/delete functionality across macOS, Windows, and Linux.

high

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

A semgrep scan flagged `scripts/postinstall.js` for calling `child_process.execSync` in a way that could become a command injection primitive if the script's execution context ever changed. The fix hardens the script by guarding its side effects behind a `require.main === module` check, introducing the safer `execFileSync` API, and adding automated tests to lock in the safe behavior.

high

How command injection happens in Node.js child_process and how to fix it

A critical command injection vulnerability in `scripts/check-links.js` was fixed by replacing `execSync()` with `execFileSync()`, eliminating shell interpretation of user-controlled repository names. This proactive hardening prevents potential remote code execution in the GitHub CLI integration workflow.

critical

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

A critical command injection vulnerability in `scripts/sync-skill.mjs` allowed attackers to execute arbitrary commands through malicious command-line arguments. The fix implements strict whitelist validation on `process.argv` inputs, ensuring only the `--check` flag is accepted before any shell interaction occurs.

high

How command injection happens in JavaScript child_process and how to fix it

A high-severity command injection vulnerability in Claude Code's `prepare-native.js` could have allowed attackers to execute arbitrary shell commands through malicious npm package tarball URLs. The fix adds strict URL scheme validation and proper curl argument termination to neutralize injection vectors.

high

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

The Spotify CLI contained a command injection vulnerability in its browser-opening functionality, where user-controlled URLs were passed directly to `exec()` with shell interpretation enabled. By switching from `exec()` to `execFile()` and properly structuring command arguments, the fix eliminates the attack surface while maintaining cross-platform compatibility.