Skip to content

Commit ffb8c9f

Browse files
fix: validate Origin header on WebSocket upgrade to prevent cross-site WebSocket hijacking (#210)
2 parents 216206e + 2534a2e commit ffb8c9f

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)