Configuration & Environment
MCP servers bound to localhost without Host header validation are reachable from any web page in the user's browser via DNS rebinding, even when bearer-token authentication is enforced.
This is a reliance-on-untrusted-inputs vulnerability (CWE-350) where an HTTP MCP server accepts requests on a loopback interface (127.0.0.1, ::1, localhost) without verifying that the inbound Host header matches an expected value. A malicious page in the user's browser can resolve attacker.com to 127.0.0.1 (the rebinding step), then issue requests that the TCP stack delivers to the local MCP process while the application sees Host: attacker.com — bypassing same-origin protections and reaching the local server with whatever credentials the browser happens to attach.
MCP HTTP transports were specifically called out by Anthropic for this attack class: the official TypeScript SDK before v1.24.0 and Python SDK both shipped HTTP transports with no Host validation by default (advisories GHSA-w48q-cv73-mx4w and GHSA-9h52-p55h-vw2f). Because MCP servers commonly run on developer machines exposing tools that read source code, run shell commands, or speak to cloud APIs, a rebound request can drive the LLM tool surface from any browser tab the user happens to leave open. Bearer-token auth alone does not help — if the token is in a previously-set cookie or header, the rebinding request carries it; the only reliable defense is a per-request Host check.
import uvicorn |
from fastmcp import FastMCP |
mcp = FastMCP("my-mcp") |
@mcp.tool() |
def echo(message: str) -> str: |
return message |
def auth_middleware(request, call_next): |
auth = request.headers.get("Authorization", "") |
if not auth.startswith("Bearer "): |
return {"status": 401} |
return call_next(request) |
# VULNERABLE: localhost bind, Bearer auth, but no Host header check. |
# DNS rebinding lets attacker.com → 127.0.0.1 reach this from a browser. |
if __name__ == "__main__": |
uvicorn.run(mcp.app, host="127.0.0.1", port=8000) |
from fastapi import FastAPI, Request, HTTPException |
from fastmcp import FastMCP |
import uvicorn |
mcp = FastMCP("guarded") |
app = FastAPI() |
ALLOWED_HOSTS = {"127.0.0.1:8000", "localhost:8000"} |
@app.middleware("http") |
async def check_host(request: Request, call_next): |
if request.headers.get("Host") not in ALLOWED_HOSTS: |
raise HTTPException(status_code=403) |
return await call_next(request) |
@mcp.tool() |
def echo(message: str) -> str: |
return message |
if __name__ == "__main__": |
uvicorn.run(app, host="127.0.0.1", port=8000) |
MCPSafe fires when ALL three conditions hold in the same file: (1) the file registers MCP tools (`@mcp.tool`, `server.tool(`, `@server.tool`); (2) an HTTP server is bound to a loopback address (`uvicorn.run(..., host="127.0.0.1")`, `app.listen(port, "localhost")`, `[::1]`, etc.); (3) no Host-validation marker is present file-wide. Markers that silence the finding include the SDK helpers `hostHeaderValidation()` and `createMcpExpressApp()` (Host check on by default in @modelcontextprotocol/sdk ≥ 1.24.0), FastAPI `TrustedHostMiddleware(allowed_hosts=...)`, custom `validate_host` / `check_host` / `assert_host` helpers, `allowed_hosts` / `host_allowlist` config keys, and any explicit `req.headers.host` / `request.headers.get("Host")` comparison. MCP-274 deliberately co-fires with MCP-268 (localhost-no-auth) when both defenses are missing, because the remediations are distinct.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now