Skip to content

Commit 2534a2e

Browse files
committed
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
1 parent 216206e commit 2534a2e

1 file changed

Lines changed: 39 additions & 0 deletions

File tree

mcp-bridge/src/daemon.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,32 @@ function log(msg: string) {
4545
process.stderr.write(`[aipex-daemon] ${msg}\n`);
4646
}
4747

48+
// ── Origin validation ───────────────────────────────────────────────────────
49+
50+
/**
51+
* Validate the Origin header on WebSocket upgrade requests to prevent
52+
* cross-site WebSocket hijacking (CSWSH).
53+
*
54+
* Allowed origins:
55+
* - No Origin header (Node.js clients: bridge.ts, cli.ts, aipex-cli)
56+
* - chrome-extension:// (the AIPex browser extension)
57+
* - moz-extension:// (Firefox extension equivalent)
58+
*
59+
* Rejected origins:
60+
* - http:// or https:// (web pages — attack vector for CSWSH)
61+
*/
62+
function isOriginAllowed(origin: string | undefined): boolean {
63+
// Node.js WebSocket clients don't send an Origin header — allow
64+
if (!origin) return true;
65+
66+
// Browser extensions are trusted clients
67+
if (origin.startsWith("chrome-extension://")) return true;
68+
if (origin.startsWith("moz-extension://")) return true;
69+
70+
// Reject all web page origins (http/https) — prevents CSWSH attacks
71+
return false;
72+
}
73+
4874
// ── Extension connection ────────────────────────────────────────────────────
4975

5076
let extensionWs: WebSocket | undefined;
@@ -325,6 +351,19 @@ const bridgeWss = new WebSocketServer({ noServer: true });
325351
const cliWss = new WebSocketServer({ noServer: true });
326352

327353
httpServer.on("upgrade", (req, socket, head) => {
354+
const origin = req.headers.origin;
355+
356+
// Reject WebSocket upgrades from web page origins to prevent CSWSH.
357+
// Legitimate clients (bridge.ts, cli.ts) are Node.js processes that
358+
// don't send an Origin header. The Chrome extension sends
359+
// chrome-extension:// which is explicitly allowed.
360+
if (!isOriginAllowed(origin)) {
361+
log(`Rejected WebSocket upgrade from origin: ${origin}`);
362+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
363+
socket.destroy();
364+
return;
365+
}
366+
328367
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
329368

330369
if (pathname === "/extension" || pathname === "/") {

0 commit comments

Comments
 (0)