Security consideration: the SDK's SECURITY.md asks for private-advisory reports, but those require repo admin rights unavailable to outside reporters — filing publicly here so this is tracked (maintainers: happy to provide details privately on request).
Bug: Host/Origin allowlist bypass via prefix (startswith(base + ":")) matching
In src/mcp/server/transport_security.py (verified in mcp 2.0.0), both _validate_host and _validate_origin accept an allowed entry using a prefix match:
if allowed.endswith(":*"):
base_host = allowed[:-2]
if host.startswith(base_host + ":"):
return True
Because this is a raw startswith(base + ":"), any header value that merely begins with the allowed host + : is accepted — the value after the colon is never validated as a real port.
Impact
- With FastMCP's baked-in loopback defaults,
allowed_hosts includes 127.0.0.1:* / localhost:*. A Host/Origin of 127.0.0.1:80.evil.com (or 127.0.0.1:anything.evil.com) is accepted, even though it is not the loopback host — defeating the DNS-rebinding / Cross-Origin restriction the middleware is meant to enforce.
- Any user-supplied host entry (e.g.
172.21.12.48:*) has the identical looseness.
- The suffix isn't even required to be a numeric port (
127.0.0.1:evil.com passes).
Repro
- Server with
enable_dns_rebinding_protection=True, allowed_hosts=["127.0.0.1:*"] (FastMCP's default for loopback).
- Send request with
Host: 127.0.0.1:evil.com.
- Middleware accepts it (returns
None) instead of 421.
Suggested fix
Validate the suffix is a numeric port only — e.g. host[len(base_host)+1:].isdigit() — or parse scheme+host+port properly (Starlette's TrustedHostMiddleware is a good reference). Apply the same fix to _validate_origin. This should also be gated so 127.0.0.1:evil.com (non-numeric) and 127.0.0.1:8080.evil.com are rejected.
Context
Found while reviewing repowise-dev/repowise#1737 — repowise correctly inherits FastMCP's existing loopback allowlist, so this is an upstream SDK issue, not a repowise defect. (Note: allowed_hosts wildcard subdomain support is already tracked in #2141; this issue is the distinct prefix-match bypass of the :* handling.)
Security consideration: the SDK's
SECURITY.mdasks for private-advisory reports, but those require repo admin rights unavailable to outside reporters — filing publicly here so this is tracked (maintainers: happy to provide details privately on request).Bug: Host/Origin allowlist bypass via prefix (
startswith(base + ":")) matchingIn
src/mcp/server/transport_security.py(verified in mcp 2.0.0), both_validate_hostand_validate_originaccept an allowed entry using a prefix match:Because this is a raw
startswith(base + ":"), any header value that merely begins with the allowed host +:is accepted — the value after the colon is never validated as a real port.Impact
allowed_hostsincludes127.0.0.1:*/localhost:*. A Host/Origin of127.0.0.1:80.evil.com(or127.0.0.1:anything.evil.com) is accepted, even though it is not the loopback host — defeating the DNS-rebinding / Cross-Origin restriction the middleware is meant to enforce.172.21.12.48:*) has the identical looseness.127.0.0.1:evil.compasses).Repro
enable_dns_rebinding_protection=True,allowed_hosts=["127.0.0.1:*"](FastMCP's default for loopback).Host: 127.0.0.1:evil.com.None) instead of421.Suggested fix
Validate the suffix is a numeric port only — e.g.
host[len(base_host)+1:].isdigit()— or parse scheme+host+port properly (Starlette'sTrustedHostMiddlewareis a good reference). Apply the same fix to_validate_origin. This should also be gated so127.0.0.1:evil.com(non-numeric) and127.0.0.1:8080.evil.comare rejected.Context
Found while reviewing repowise-dev/repowise#1737 — repowise correctly inherits FastMCP's existing loopback allowlist, so this is an upstream SDK issue, not a repowise defect. (Note:
allowed_hostswildcard subdomain support is already tracked in #2141; this issue is the distinct prefix-match bypass of the:*handling.)