Skip to content

Unauthenticated MCP control via POST /sse — reachable from LAN and from any website #54

Description

@aaronjmars

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:

  1. LAN reachability — any peer on the network can list and call tools.
  2. Any visited websiteAccess-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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions