Root cause
Other / insufficient public mechanism detail introduced
fix: add CORS/OPTIONS support for Llama.cpp compatibility (issue #178)
How AI contributed: Direct introductionPublished May 21, 2026 in
cursortouch/windows-mcp
Windows MCP exposes PowerShell control over its HTTP and SSE transports, and a Claude-assisted change added permissive cross-origin handling in src/windows_mcp/__main__.py. The middleware installed CORSMiddleware with wildcard origins, methods, and headers plus an OPTIONS responder that answered every preflight, letting any website send cross-origin requests to it. With no authentication on that transport, the exposed PowerShell tools were reachable. The fix released in 0.7.5 replaces the wildcard with an opt-in cors_origins list and adds TrustedHostMiddleware to block DNS rebinding.
The middleware registered CORSMiddleware with allow_origins set to a wildcard and added an OPTIONS handler that returned successful preflight responses, so browsers were told that any origin could read responses from the local server. Because the HTTP transport also accepted unauthenticated requests, a malicious page could drive the PowerShell command tools directly.
Root cause
fix: add CORS/OPTIONS support for Llama.cpp compatibility (issue #178)
Fix
fix: remove wildcard CORS and add DNS rebinding protection (GHSA-vrxg-gm77-7q5g)
AI-assisted fix: Claude Code · jeomon
Case facts
@@ -2,6 +2,8 @@ from windows_mcp.auth import AuthClient from windows_mcp.config import is_debug, enable_debug from fastmcp import FastMCP+from starlette.middleware import Middleware+from starlette.middleware.cors import CORSMiddleware from dataclasses import dataclass, field from textwrap import dedent from enum import Enum@@ -33,6 +35,57 @@ def _get_analytics(): return analytics +def _http_middleware() -> list:+ """Return ASGI middleware for HTTP transports including CORS and OPTIONS handling."""+ return [+ Middleware(OptionsMiddleware),+ Middleware(+ CORSMiddleware,+ allow_origins=["*"],+ allow_methods=["*"],+ allow_headers=["*"],+ ),+ ]+++class OptionsMiddleware:+ """ASGI middleware that intercepts OPTIONS requests and returns 200 OK."""++ def __init__(self, app: Any) -> None:+ self.app = app++ async def __call__(self, scope: Any, receive: Any, send: Any) -> None:+ if scope["type"] == "http" and scope["method"] == "OPTIONS":+ await send(+ {+ "type": "http.response.start",+ "status": 200,+ "headers": [+ [b"content-length", b"0"],+ [b"access-control-allow-origin", b"*"],+ [b"access-control-allow-methods", b"*"],+ [b"access-control-allow-headers", b"*"],+ ],+ }+ )+ await send(+ {+ "type": "http.response.body",+ "body": b"",+ }+ )+ else:+ await self.app(scope, receive, send)+++def _add_cors_options_handlers(mcp: FastMCP) -> None:+ """Register custom route handlers for CORS OPTIONS requests."""+ # Note: Custom routes are registered via the http_app() method when+ # the app is created, so we don't need to do anything here with the new+ # OptionsMiddleware approach. The middleware is passed via run().+ pass++ def _build_local_mcp() -> FastMCP: """Create the local desktop-control server only when local mode is used.""" global _local_mcp@@ -71,6 +124,7 @@ async def lifespan(app: FastMCP): _local_mcp = FastMCP(name="windows-mcp", instructions=instructions, lifespan=lifespan) register_all(_local_mcp, get_desktop=_get_desktop, get_analytics=_get_analytics)+ _add_cors_options_handlers(_local_mcp) return _local_mcp @@ -120,7 +174,13 @@ def _run_local_mode(transport: str, host: str, port: int) -> None: case Transport.STDIO.value: local_mcp.run(transport=Transport.STDIO.value, show_banner=False) case Transport.SSE.value | Transport.STREAMABLE_HTTP.value:- local_mcp.run(transport=transport, host=host, port=port, show_banner=False)+ local_mcp.run(+ transport=transport,+ host=host,+ port=port,+ show_banner=False,+ middleware=_http_middleware(),+ ) case _: raise ValueError(f"Invalid transport: {transport}") @@ -21,6 +21,7 @@ class ServerConfig: @dataclass class SecurityConfig: ip_allowlist: list[str] = field(default_factory=list)+ cors_origins: list[str] = field(default_factory=list) oauth_client_id: str | None = None oauth_client_secret: str | None = None @@ -109,6 +110,8 @@ def load_config(path: Path | None) -> WindowsMCPConfig: if "ip_allowlist" in security: cfg.security.ip_allowlist = _list_of_strings(security["ip_allowlist"], "security.ip_allowlist")+ if "cors_origins" in security:+ cfg.security.cors_origins = _list_of_strings(security["cors_origins"], "security.cors_origins") if "oauth_client_id" in security: cfg.security.oauth_client_id = str(security["oauth_client_id"]) or None if "oauth_client_secret" in security:@@ -149,6 +152,9 @@ def write_config(cfg: WindowsMCPConfig, path: Path) -> None: if sec.ip_allowlist: items = ', '.join(f'"{ip}"' for ip in sec.ip_allowlist) sec_lines.append(f'ip_allowlist = [{items}]')+ if sec.cors_origins:+ items = ', '.join(f'"{o}"' for o in sec.cors_origins)+ sec_lines.append(f'cors_origins = [{items}]') if sec.oauth_client_id: sec_lines.append(f'oauth_client_id = "{sec.oauth_client_id}"') if sec.oauth_client_secret:@@ -57,12 +57,26 @@ def _http_middleware( auth_key: str | None = None, ip_allowlist: list | None = None, oauth_validator=None,+ cors_origins: list[str] | None = None,+ allowed_hosts: list[str] | None = None, ) -> list:- """Return ASGI middleware for HTTP transports including CORS and OPTIONS handling."""- middleware = [- Middleware(OptionsMiddleware),- Middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]),+ """Return ASGI middleware for HTTP transports."""+ middleware: list = [+ Middleware(OptionsMiddleware, allowed_origins=cors_origins or []), ]+ if allowed_hosts:+ from starlette.middleware.trustedhost import TrustedHostMiddleware+ middleware.append(Middleware(TrustedHostMiddleware, allowed_hosts=allowed_hosts))+ if cors_origins:+ middleware.append(+ Middleware(+ CORSMiddleware,+ allow_origins=cors_origins,+ allow_methods=["GET", "POST", "OPTIONS"],+ allow_headers=["Content-Type", "Authorization", "Mcp-Session-Id"],+ allow_credentials=False,+ )+ ) if ip_allowlist: middleware.append(Middleware(IPAllowlistMiddleware, allowlist=ip_allowlist)) if auth_key:@@ -86,31 +100,34 @@ def _choose_value(ctx: click.Context, name: str, cli_value, config_value, defaul class OptionsMiddleware:- """ASGI middleware that intercepts OPTIONS requests and returns 200 OK."""+ """ASGI middleware that intercepts OPTIONS preflight requests. - def __init__(self, app: Any) -> None:+ Only echoes CORS headers when the request Origin is in the explicit allowlist.+ With an empty allowlist (default), returns 200 OK with no CORS headers so that+ browsers block cross-origin access via Same-Origin Policy.+ """++ def __init__(self, app: Any, *, allowed_origins: list[str] | None = None) -> None: self.app = app+ self._allowed: frozenset[str] = frozenset(allowed_origins or []) async def __call__(self, scope: Any, receive: Any, send: Any) -> None: if scope["type"] == "http" and scope["method"] == "OPTIONS":- await send(- {- "type": "http.response.start",- "status": 200,- "headers": [- [b"content-length", b"0"],- [b"access-control-allow-origin", b"*"],- [b"access-control-allow-methods", b"*"],- [b"access-control-allow-headers", b"*"],- ],- }- )- await send(- {- "type": "http.response.body",- "body": b"",- }- )+ headers: list[list[bytes]] = [[b"content-length", b"0"]]+ if self._allowed:+ origin = next(+ (v.decode("latin-1") for k, v in scope.get("headers", []) if k == b"origin"),+ None,+ )+ if origin and origin in self._allowed:+ headers += [+ [b"access-control-allow-origin", origin.encode("latin-1")],+ [b"access-control-allow-methods", b"GET, POST, OPTIONS"],+ [b"access-control-allow-headers", b"content-type, authorization, mcp-session-id"],+ [b"vary", b"Origin"],+ ]+ await send({"type": "http.response.start", "status": 200, "headers": headers})+ await send({"type": "http.response.body", "body": b""}) else: await self.app(scope, receive, send) @@ -223,6 +240,8 @@ def _run_server( ssl_certfile: str | None = None, ssl_keyfile: str | None = None, oauth_validator=None,@@ -145,10 +145,13 @@ These tools only read information without making changes: ### 4. **Network Security** -- When using SSE or HTTP transport modes, ensure proper network isolation-- Use localhost binding (`127.0.0.1`) instead of `0.0.0.0` when possible+- The default `stdio` transport has no network exposure — prefer it for local use+- HTTP transports (`sse`, `streamable-http`) enforce authentication for any non-loopback bind address; starting without `--auth-key`, `--oauth-client-id/secret`, or `--allow-insecure-remote` on a non-loopback host is refused at startup+- **CORS is disabled by default** — no `Access-Control-Allow-Origin` headers are emitted, so browsers block cross-origin requests via their own Same-Origin Policy; use `--cors-origins` only if you need a browser-based MCP client and restrict it to the specific origin(s) required+- **DNS rebinding protection** is applied automatically — the server validates the `Host` header against the configured bind address so a DNS rebinding attack cannot be used to reach a localhost-bound server from an external site+- Use localhost / loopback binding (`127.0.0.1`) instead of `0.0.0.0` when network-wide access is not required - Implement firewall rules to restrict access to the MCP server ports-- Never expose the MCP server directly to the internet without proper authentication+- Never expose the MCP server to the internet without `--auth-key` or OAuth and TLS (`--ssl-certfile`/`--ssl-keyfile`) ### 5. **Data Protection** @@ -248,10 +251,10 @@ Windows-MCP relies on several third-party libraries. We: ### Key Dependencies -- **PyAutoGUI**: Mouse and keyboard automation-- **UIAutomation**: Windows UI interaction-- **FastMCP**: MCP server framework-- **httpx**: HTTP client for web scraping+- **comtypes / uia**: Low-level Windows UIAutomation COM bindings for UI interaction and accessibility-tree traversal+- **FastMCP**: MCP server framework (stdio, SSE, Streamable HTTP transports)+- **Starlette / Uvicorn**: ASGI layer used by the HTTP transports; provides CORS, TrustedHost, and custom auth middleware+- **httpx**: HTTP client used by the Scrape tool for web content fetching ## Compliance and Auditing @@ -293,7 +296,7 @@ All tools include security-relevant annotations: - **idempotentHint**: `true` if repeated calls have no additional effect - **openWorldHint**: `true` if the tool interacts with external entities -Refer to `main.py` for complete tool annotations.+Refer to `src/windows_mcp/__main__.py` and `src/windows_mcp/tools/` for complete tool annotations. ## Disclaimer AI-assisted change 6a222468e31c52acf0b4cc8b728df22990f230b03d2162c2871d8c44cb3ecf59 · Fix dbb30d48be168afaf0a8f35f0cfcc5065bc37b81b0395af253c988428dde9356