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

Frequently Asked Questions

What is arbitrary code execution via command injection in shell-quote?

It's a flaw where shell-quote's escaping logic (`quote()`/`parse()`) failed to neutralize Unicode line terminator characters (U+2028, U+2029), letting crafted input break out of shell quoting and run attacker-supplied commands.

How do you prevent command injection in Node.js?

Avoid building shell command strings from untrusted input, use APIs like `child_process.execFile`/`spawn` with an argument array (no shell), keep quoting/escaping libraries patched, and pin known-vulnerable transitive dependencies to safe versions.

What CWE is command injection?

Command injection maps to CWE-78 (Improper Neutralization of Special Elements used in an OS Command), sometimes alongside CWE-88 (Argument Injection) for argument-level escaping bugs like this one.

Is upgrading the dependency version alone enough to prevent this vulnerability?

Yes for this specific flaw — upgrading/pinning `shell-quote` to `1.8.4` includes the fix that properly escapes line terminator characters, but you should still avoid passing untrusted input directly into shell command construction wherever possible.

Can static analysis detect this vulnerability?

Yes — software composition analysis (SCA) tools like Trivy, npm audit, and Snyk flag known-vulnerable versions of packages like shell-quote by matching against CVE databases and lockfile contents.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #751

Related Articles

critical

How command injection happens in Kotlin/Android and how to fix it

V2rayNG's RootShell.kt built root shell commands by concatenating an unescaped file path directly into a string passed to `su -c`, creating a critical command injection risk (CWE-78). The fix restricts the `exec()` API to internal use only and single-quote-escapes the file path before it ever reaches the root shell.

high

How shell command injection happens in Ruby and how to fix it

A critical command injection vulnerability was discovered in Fastlane's deliver module where `system("open '#{html_path}'")` allowed shell metacharacters in file paths to execute arbitrary commands. The fix replaces vulnerable string interpolation with array-based argument passing, eliminating the shell entirely.

critical

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

CVE-2026-9277 is a critical command injection vulnerability in shell-quote versions prior to 1.8.4 that allows attackers to execute arbitrary code by injecting unescaped line terminators into shell commands. This vulnerability affects any Node.js application that uses the vulnerable shell-quote package to construct shell commands from untrusted input. The fix upgrades shell-quote to version 1.8.4, which properly escapes line terminators and neutralizes the injection vector.

high

How command injection happens in Ruby and how to fix it

A Fastlane helper used a Ruby backtick subshell to clone a plugin's git repository, interpolating `self.homepage` directly into a shell command string. Even with `shellescape` applied, the pattern was flagged as a dangerous subshell that could be chained into a command injection primitive; the fix replaces it with `system()` using an argument array, eliminating shell interpretation entirely.

critical

How command injection happens in JavaScript dependency trees and how to fix it

A critical command injection vulnerability in websocket-driver 0.7.4 allowed attackers to execute arbitrary shell commands through unescaped line terminators in WebSocket protocol handling. The automated fix upgrades to version 0.7.5 and adds an explicit override in package.json to prevent dependency resolution from reverting to the vulnerable version.

critical

How Cross-Site Scripting happens in fast-xml-parser and how to fix it

CVE-2026-25896 is a critical Cross-Site Scripting vulnerability in fast-xml-parser stemming from improper DOCTYPE entity handling, which could allow attackers to inject malicious scripts through crafted XML payloads. The fix upgrades the vulnerable dependency from version 4.4.1 to patched versions 5.3.5 and 4.5.4, eliminating the unsafe parsing behavior while preserving all legitimate XML processing functionality.