From 22ee111278ffe3c0b1a51e5f1fd117052918ca0f Mon Sep 17 00:00:00 2001 From: "@aaronjmars" <61592645+aaronjmars@users.noreply.github.com> Date: Mon, 20 Jul 2026 10:51:03 -0400 Subject: [PATCH] fix(security): bind the extension control channel to loopback and gate its handshake The WebSocket server was created as `new WebSocket.Server({ port: WS_PORT })`. With no `host`, Node listens on all interfaces, so the browser-automation control channel was reachable from the LAN. The connection handler hands `chromeExtensionSocket` to whoever connects last, closing the previous socket, so any reachable peer could evict the real extension, receive every automation command the MCP server issues, and return fabricated results to the model. Two changes: - Bind to 127.0.0.1. The extension connects to ws://localhost:5555 from the same machine and the optional ngrok tunnel forwards the HTTP port only, so nothing legitimate needs this off-host. - Add a verifyClient gate. Loopback alone is not sufficient: a web page the user visits can also reach 127.0.0.1. Browsers send Origin on a cross-origin WebSocket handshake, so page-initiated connections are rejected while extension-scheme origins and local non-browser clients are allowed. Verified against a running server: bind address 127.0.0.1:5711 (was *:5711) loopback + chrome-extension:// ACCEPTED loopback + no origin ACCEPTED loopback + https://evil.example REJECTED 403 LAN 10.x.x.x ECONNREFUSED Not addressed here: the HTTP/SSE server still listens on all interfaces with `Access-Control-Allow-Origin: *`. That one needs a separate decision because remote MCP clients and the tunnel path may depend on it. Co-Authored-By: Claude --- opendia-mcp/server.js | 45 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/opendia-mcp/server.js b/opendia-mcp/server.js index c8e8f09..1827c3d 100755 --- a/opendia-mcp/server.js +++ b/opendia-mcp/server.js @@ -1571,6 +1571,38 @@ function handleToolResponse(message) { } } +// Gate the control-channel handshake. +// +// The loopback bind already keeps other machines out. This closes the remaining +// hole: a web page the user visits can also reach 127.0.0.1, and a page that +// connects would be handed the extension's slot. Browsers always send Origin on +// a cross-origin WebSocket handshake, so a page announces itself as http(s). +// Extension service workers send an extension-scheme Origin; local non-browser +// clients (test-extension.js) send none. +function isLoopback(address) { + return ( + address === "127.0.0.1" || + address === "::1" || + address === "::ffff:127.0.0.1" + ); +} + +function verifyExtensionClient(info, done) { + const remote = info.req.socket.remoteAddress; + if (!isLoopback(remote)) { + console.error(`🚫 Rejected WebSocket handshake from non-loopback ${remote}`); + return done(false, 403, "Forbidden"); + } + + const origin = info.origin || info.req.headers.origin; + if (origin && !/^(chrome|moz|safari-web)-extension:\/\//.test(origin)) { + console.error(`🚫 Rejected WebSocket handshake from origin ${origin}`); + return done(false, 403, "Forbidden"); + } + + return done(true); +} + // Setup WebSocket connection handlers function setupWebSocketHandlers() { wss.on("connection", (ws) => { @@ -1817,8 +1849,17 @@ async function startServer() { console.error(`🔄 HTTP port adjusted to ${HTTP_PORT} to avoid WebSocket conflict`); } - // Initialize WebSocket server after port resolution - wss = new WebSocket.Server({ port: WS_PORT }); + // Initialize WebSocket server after port resolution. + // Bound to loopback: this socket is the browser-automation control channel, and + // whoever holds it becomes chromeExtensionSocket (see setupWebSocketHandlers). + // The extension connects to ws://localhost:5555 from the same machine, and the + // optional ngrok tunnel forwards the HTTP port only, so nothing legitimate + // needs this reachable off-host. + wss = new WebSocket.Server({ + port: WS_PORT, + host: "127.0.0.1", + verifyClient: verifyExtensionClient, + }); // Set up WebSocket connection handling setupWebSocketHandlers();