fix(serve): enable Host/Origin protection on the --http MCP server#671
fix(serve): enable Host/Origin protection on the --http MCP server#671spartan8806 wants to merge 1 commit into
Conversation
|
This currently crashes before the HTTP server starts. The locked FastMCP 3.2.4 run_http_async signature does not accept host_origin_protection; binding that keyword raises TypeError: unexpected keyword argument. The mock-only test cannot catch the production failure. Please use a supported middleware/server configuration or intentionally upgrade FastMCP, then test foreign Origin rejection plus same-Origin and no-Origin clients end to end. |
`serve --http` starts a FastMCP streamable-http server on 127.0.0.1:5555. A loopback bind is not by itself an access control for a browser: a page the user visits can point a hostname it controls at 127.0.0.1 (DNS rebinding) and then drive the MCP tools, which read the user's source tree. Add a pure-ASGI `LoopbackOriginGuard`, passed through `run_http_async`'s supported `middleware=[...]` parameter, so nothing outside the pinned FastMCP 3.2.4 API is required. It is pure ASGI rather than `BaseHTTPMiddleware` because streamable-http holds long-lived streaming/SSE responses open, which `BaseHTTPMiddleware` interferes with. The guard rejects a `Host` that is not the loopback endpoint -- this is what stops DNS rebinding, since the rebound request carries the attacker's hostname -- and rejects a present `Origin` that is not the loopback endpoint. An absent `Origin` is allowed, because non-browser MCP clients do not send one. The guard engages only for a loopback bind; `--host 0.0.0.0` is an explicit decision to expose the endpoint and passes through untouched. Tests exercise the real FastMCP ASGI app (`mcp.http_app(middleware=...)`, which is what `run_http_async` builds) rather than a mock: - foreign Origin over http and https -> 403 - same-origin and localhost Origin -> not blocked - no Origin at all (ordinary MCP client) -> not blocked - rebound Host, with and without the port -> 403 - Origin on the wrong port / non-http -> 403 A signature-contract test binds the kwargs the entry point passes against the real `FastMCP.run_http_async` signature, so a keyword the pinned FastMCP does not accept cannot pass review behind a mocked `mcp.run`.
ebe8e12 to
6fec1c9
Compare
|
You're right, and thanks for catching it. I confirmed it against the locked FastMCP 3.2.4: I've replaced it with a supported configuration on 3.2.4, with no dependency change. What changed
Tests End-to-end against the real FastMCP ASGI app (
There is also a signature-contract test that binds the kwargs the entry point passes against the real Checks on the locked toolchain
On the full suite I see 18 failures locally (Julia/HCL parser reconciliation, and the symlink / file-mode tests), but they reproduce identically with this change stashed, so they're pre-existing on my Windows environment rather than anything this PR touches. Happy to narrow the scope or drop it entirely if you'd rather not carry the middleware. |
Small hardening for the opt-in HTTP mode. serve --http starts a FastMCP streamable-http server on 127.0.0.1:5555, but FastMCP's host_origin_protection defaults to off — so the endpoint accepts requests from any Origin. A web page the user visits could reach the loopback port via DNS rebinding and drive the MCP tools (which read the codebase).
This passes host_origin_protection="auto" so FastMCP validates Host/Origin for the loopback-bound server. Verified locally: a foreign Origin now gets 403 Forbidden Origin, while non-browser clients (no Origin header) and same-origin requests keep working. Default stdio transport is unaffected.
Low impact — opt-in --http, read-only tools, requires rebinding — but a cheap defense-in-depth. Hope it's useful! 🙂
Checklist