Static review of public source at commit 6affe5c0d358. No traffic was sent to any MCP environment.
TransportSecurityMiddleware treats a missing settings argument as “protection off”, even though the settings model itself defaults enable_dns_rebinding_protection to True:
src/mcp/server/transport_security.py (model default + constructor):
enable_dns_rebinding_protection: bool = True
...
def __init__(self, settings: TransportSecuritySettings | None = None):
# If not specified, disable DNS rebinding protection by default for backwards compatibility
self.settings = settings or TransportSecuritySettings(enable_dns_rebinding_protection=False)
StreamableHTTPServerTransport passes that through unchanged (security_settings: ... = None → TransportSecurityMiddleware(security_settings) at the transport constructor).
MCPServer.streamable_http_app / sse_app only auto-enable an allowlist when host is loopback (127.0.0.1 / localhost / ::1) — see src/mcp/server/lowlevel/server.py around the auto-enable block. Binding or mounting with 0.0.0.0, a LAN IP, or a reverse-proxy hostname therefore ships without Host/Origin checks unless the operator remembers to pass TransportSecuritySettings explicitly.
DNS rebinding against a browser-reachable MCP HTTP transport is exactly what those checks are for. Fail-open on the common “I mounted the ASGI app / bound all interfaces” path is the surprising default.
Suggested change:
- When
security_settings is None, enable protection with a documented default allowlist (at least the bind host), or refuse to serve HTTP transports until settings are provided.
- Keep an explicit opt-out (
enable_dns_rebinding_protection=False) for demos that truly need it.
- Log once at startup when protection is disabled.
Severity: medium as insecure default / defense-in-depth for HTTP transports; not claiming a working exploit against a specific deployment. No proof-of-concept.
Happy to send a focused PR if this direction is useful.
Static review of public source at commit
6affe5c0d358. No traffic was sent to any MCP environment.TransportSecurityMiddlewaretreats a missingsettingsargument as “protection off”, even though the settings model itself defaultsenable_dns_rebinding_protectiontoTrue:src/mcp/server/transport_security.py(model default + constructor):StreamableHTTPServerTransportpasses that through unchanged (security_settings: ... = None→TransportSecurityMiddleware(security_settings)at the transport constructor).MCPServer.streamable_http_app/sse_apponly auto-enable an allowlist whenhostis loopback (127.0.0.1/localhost/::1) — seesrc/mcp/server/lowlevel/server.pyaround the auto-enable block. Binding or mounting with0.0.0.0, a LAN IP, or a reverse-proxy hostname therefore ships without Host/Origin checks unless the operator remembers to passTransportSecuritySettingsexplicitly.DNS rebinding against a browser-reachable MCP HTTP transport is exactly what those checks are for. Fail-open on the common “I mounted the ASGI app / bound all interfaces” path is the surprising default.
Suggested change:
security_settings is None, enable protection with a documented default allowlist (at least the bind host), or refuse to serve HTTP transports until settings are provided.enable_dns_rebinding_protection=False) for demos that truly need it.Severity: medium as insecure default / defense-in-depth for HTTP transports; not claiming a working exploit against a specific deployment. No proof-of-concept.
Happy to send a focused PR if this direction is useful.