CVE-2026-59950: WebSocket Origin/Host Validation Added, but Secure-by-Default Gap Remains
Summary
The patch wires the deprecated WebSocket server transport into the existing transport security validator so Host and Origin checks can run before handshake acceptance. This addresses the immediate missing-validation flaw, but the new behavior is opt-in via security_settings and the added tests explicitly confirm that default settings still accept arbitrary Origin values. As a result, the code change fixes the root missing control in the transport implementation, while leaving deployment safety dependent on caller configuration.
Analysis
Vulnerability
CVE-2026-59950 describes a cross-site WebSocket hijacking issue in the Model Context Protocol Python SDK's deprecated WebSocket server transport. Per the advisory and CVE records, the transport failed to validate Origin and Host headers, allowing a malicious website to induce a victim browser to open a WebSocket connection to a local MCP server and issue unauthorized protocol actions. See the official advisory at GHSA-VJ7Q-GJH5-988W, the fixing commit 777b8d06710c140e3606b0d4598e2aa48546c266, and the related PR #2992.
The root defect was architectural inconsistency: SSE and Streamable HTTP already had transport security settings, but the deprecated WebSocket transport did not invoke the same validation path before accepting the handshake. The patch summary shows the validator signature widened from Request to HTTPConnection, which is necessary because WebSocket connections are not plain HTTP request objects but still expose headers needed for policy enforcement.
from starlette.requests import HTTPConnection
async def validate_request(self, request: HTTPConnection, is_post: bool = False) -> Response | None:That change is security-relevant because it allows one validation implementation to operate across HTTP and WebSocket connection types, enabling consistent Host/Origin enforcement at the transport boundary.
Patch
The patch integrates the WebSocket server transport with the existing TransportSecurityMiddleware and adds an optional security_settings parameter to websocket_server(...). Before the handshake is accepted, the code constructs the security middleware, validates the incoming WebSocket connection, and if validation fails, closes the socket and raises ValueError. The patch notes that the ASGI server maps this pre-accept close to HTTP 403.
from mcp.server.transport_security import TransportSecurityMiddleware, TransportSecuritySettings
async def websocket_server(
scope: Scope,
receive: Receive,
send: Send,
security_settings: TransportSecuritySettings | None = None,
):
security = TransportSecurityMiddleware(security_settings)
error_response = await security.validate_request(websocket, is_post=False)
if error_response is not None:
await websocket.close()
raise ValueError("Request validation failed")The tests added in the commit and PR #2992 verify three important behaviors: invalid Origin is rejected with 403 when allowed_origins is configured; invalid Host is rejected with 403 when allowed_hosts is configured; and matching origins are accepted. However, the suite also explicitly documents that with no security settings, the WebSocket transport still accepts any Origin, matching the current default behavior of the other transports.
settings = TransportSecuritySettings(
enable_dns_rebinding_protection=True,
allowed_hosts=["127.0.0.1:*"],
allowed_origins=["http://localhost:*"]
)Review
Pros
- The patch addresses the actual missing enforcement point by validating the WebSocket connection before handshake completion, which is the correct place to stop browser-originated cross-site connections.
- Reusing
TransportSecurityMiddlewarereduces policy drift across transports and avoids duplicating Host/Origin parsing logic. - Changing the validator input type from
RequesttoHTTPConnectionis a clean compatibility move that enables shared validation for both HTTP and WebSocket paths. - The tests are meaningful and security-focused: they assert 403 on invalid origin and host, and they exercise the transport end-to-end through an actual ASGI server.
- The patch is narrowly scoped to the vulnerable transport and does not appear to alter protocol semantics beyond pre-handshake rejection.
Cons
- The protection is not secure by default. The added test
test_ws_security_default_settingsexplicitly confirms that arbitrary origins are still accepted whensecurity_settingsis omitted. - Because the vulnerable transport is deprecated but still present, many existing consumers may continue using it without passing
security_settings, leaving exploitable deployments unchanged after upgrade unless they also change application code or configuration. - The rejection path depends on framework/server behavior mapping a pre-accept close to HTTP 403. The tests cover uvicorn behavior, but this is still a coupling worth documenting for portability expectations.
- The patch summary does not show any forced default such as loopback-only origin allowlists, mandatory host validation, or automatic hardening for local-server use cases, so operational safety remains caller-dependent.
Verdict
Partial fix.
The code change fixes the immediate implementation gap: the WebSocket transport can now perform the same Host/Origin validation as the other transports, and it does so before the handshake is accepted. That is a real remediation of the missing control identified in the advisory.
However, the patch does not make the transport safe by default. The included tests explicitly preserve permissive behavior when security_settings is absent, which means upgraded applications remain vulnerable if maintainers do not opt in to validation. For engineering teams, the practical conclusion is: upgrade to a version containing the fix, but also ensure the deprecated WebSocket server is instantiated with restrictive TransportSecuritySettings or, preferably, migrate off the deprecated transport entirely. The CVE context from CVE and NVD supports that this is a browser-mediated local attack surface where defaults matter materially.
Recommended Labs
Try this vulnerability pattern yourself with hands-on labs.
- CSRF.py
CVE-2026-59950 is a browser-mediated cross-site attack against a local WebSocket endpoint, caused by missing Origin and Host validation. While not a classic form-post CSRF bug, the defensive pattern is closely related: reject untrusted cross-site requests and enforce request provenance. This Python lab is the best language-aligned hands-on defensive exercise for practicing those protections.
- CORS.js
This lab focuses on cross-origin trust boundaries, which maps well to the root cause in the MCP WebSocket issue: the server trusted browser-originated requests without sufficient origin checks. Even though CORS and WebSocket origin checks are not identical controls, this lab reinforces the same defensive reasoning about who is allowed to communicate with a browser-exposed service.
- CORS.ts
This is another strong defensive lab for practicing incorrect origin/host trust assumptions in browser-connected applications. It complements the Python CSRF lab by emphasizing policy enforcement at the application boundary, which is directly relevant when reviewing patches that add Origin and Host header validation to WebSocket transports.