Follow-up to #53, which bound the WebSocket control channel to loopback and gated its handshake. The HTTP/SSE side is still open.
What is exposed
opendia-mcp/server.js:1697 mounts POST /sse, which forwards any JSON-RPC body straight to handleMCPRequest with no authentication. The Express app listens on all interfaces (server.js:1870), and CORS is wide open (server.js:1704, server.js:1756).
Once the extension is connected, anyone who can reach that port can drive the browser through the legitimate extension — the loopback bind on the WS side does not help here.
Verified against a running server
$ curl -s -X POST http://<LAN-IP>:5556/sse \
-H "Content-Type: application/json" \
-H "Origin: https://evil.example" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"page_analyze",...
$ curl -s -i -X OPTIONS http://<LAN-IP>:5556/sse \
-H "Origin: https://evil.example" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: content-type"
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Headers: content-type
Two consequences:
- LAN reachability — any peer on the network can list and call tools.
- Any visited website —
Access-Control-Allow-Origin: * plus a permissive preflight means a page the user visits can cross-origin POST to http://localhost:5556/sse and read the response. That is full browser control from arbitrary web content.
GET /ports (server.js:1814) is likewise LAN-reachable and advertises the port layout.
Why this was not fixed in #53
The tunnel path (--tunnel / ngrok, server.js:1882) forwards the HTTP port, and remote MCP clients are a supported use case, so the fix is a product decision rather than a one-line bind change. Options worth weighing:
- Default the HTTP server to
127.0.0.1 and require an explicit opt-in flag to widen it.
- Replace
Access-Control-Allow-Origin: * with an allowlist, so a visited page cannot read responses.
- Require a bearer token on
POST /sse, generated at startup and printed to the console, for the tunnel/remote-client path.
Likely some combination: loopback by default, token required whenever the bind is widened.
Follow-up to #53, which bound the WebSocket control channel to loopback and gated its handshake. The HTTP/SSE side is still open.
What is exposed
opendia-mcp/server.js:1697mountsPOST /sse, which forwards any JSON-RPC body straight tohandleMCPRequestwith no authentication. The Express app listens on all interfaces (server.js:1870), and CORS is wide open (server.js:1704,server.js:1756).Once the extension is connected, anyone who can reach that port can drive the browser through the legitimate extension — the loopback bind on the WS side does not help here.
Verified against a running server
Two consequences:
Access-Control-Allow-Origin: *plus a permissive preflight means a page the user visits can cross-originPOSTtohttp://localhost:5556/sseand read the response. That is full browser control from arbitrary web content.GET /ports(server.js:1814) is likewise LAN-reachable and advertises the port layout.Why this was not fixed in #53
The tunnel path (
--tunnel/ ngrok,server.js:1882) forwards the HTTP port, and remote MCP clients are a supported use case, so the fix is a product decision rather than a one-line bind change. Options worth weighing:127.0.0.1and require an explicit opt-in flag to widen it.Access-Control-Allow-Origin: *with an allowlist, so a visited page cannot read responses.POST /sse, generated at startup and printed to the console, for the tunnel/remote-client path.Likely some combination: loopback by default, token required whenever the bind is widened.