fix: validate Origin header on WebSocket upgrade to prevent CSWSH

The MCP daemon WebSocket server accepts connections on all three
endpoints (/bridge, /cli, /extension) without checking the Origin
header. This allows a malicious web page to open a WebSocket to
ws://127.0.0.1:9223/bridge and send tool calls that execute browser
automation (navigate, click, read page content, take screenshots,
access bookmarks/history).

Add Origin header validation in the HTTP upgrade handler:
- Allow connections with no Origin (Node.js clients: bridge.ts, cli.ts)
- Allow chrome-extension:// and moz-extension:// origins
- Reject all http:// and https:// origins with 403 Forbidden

This prevents cross-site WebSocket hijacking (CSWSH) where JavaScript
on an attacker-controlled page connects to the local daemon.

CWE-319
This commit is contained in:
Sebastion
2026-04-03 08:42:54 +01:00
parent 216206ea7b
commit 2534a2e608

View File

@@ -45,6 +45,32 @@ function log(msg: string) {
process.stderr.write(`[aipex-daemon] ${msg}\n`);
}
// ── Origin validation ───────────────────────────────────────────────────────
/**
* Validate the Origin header on WebSocket upgrade requests to prevent
* cross-site WebSocket hijacking (CSWSH).
*
* Allowed origins:
* - No Origin header (Node.js clients: bridge.ts, cli.ts, aipex-cli)
* - chrome-extension:// (the AIPex browser extension)
* - moz-extension:// (Firefox extension equivalent)
*
* Rejected origins:
* - http:// or https:// (web pages — attack vector for CSWSH)
*/
function isOriginAllowed(origin: string | undefined): boolean {
// Node.js WebSocket clients don't send an Origin header — allow
if (!origin) return true;
// Browser extensions are trusted clients
if (origin.startsWith("chrome-extension://")) return true;
if (origin.startsWith("moz-extension://")) return true;
// Reject all web page origins (http/https) — prevents CSWSH attacks
return false;
}
// ── Extension connection ────────────────────────────────────────────────────
let extensionWs: WebSocket | undefined;
@@ -325,6 +351,19 @@ const bridgeWss = new WebSocketServer({ noServer: true });
const cliWss = new WebSocketServer({ noServer: true });
httpServer.on("upgrade", (req, socket, head) => {
const origin = req.headers.origin;
// Reject WebSocket upgrades from web page origins to prevent CSWSH.
// Legitimate clients (bridge.ts, cli.ts) are Node.js processes that
// don't send an Origin header. The Chrome extension sends
// chrome-extension:// which is explicitly allowed.
if (!isOriginAllowed(origin)) {
log(`Rejected WebSocket upgrade from origin: ${origin}`);
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
socket.destroy();
return;
}
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
if (pathname === "/extension" || pathname === "/") {