diff --git a/src/mcp/server/fastmcp/server.py b/src/mcp/server/fastmcp/server.py index 6fb0ec45..f74b6555 100644 --- a/src/mcp/server/fastmcp/server.py +++ b/src/mcp/server/fastmcp/server.py @@ -174,6 +174,14 @@ class FastMCP(Generic[LifespanResultT]): auth: AuthSettings | None = None, transport_security: TransportSecuritySettings | None = None, ): + # Auto-enable DNS rebinding protection for localhost (IPv4 and IPv6) + if transport_security is None and host in ("127.0.0.1", "localhost", "::1"): + transport_security = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=["127.0.0.1:*", "localhost:*", "[::1]:*"], + allowed_origins=["http://127.0.0.1:*", "http://localhost:*", "http://[::1]:*"], + ) + self.settings = Settings( debug=debug, log_level=log_level, diff --git a/tests/server/fastmcp/test_server.py b/tests/server/fastmcp/test_server.py index fdbb0469..3935f3bd 100644 --- a/tests/server/fastmcp/test_server.py +++ b/tests/server/fastmcp/test_server.py @@ -12,6 +12,7 @@ from mcp.server.fastmcp.prompts.base import Message, UserMessage from mcp.server.fastmcp.resources import FileResource, FunctionResource from mcp.server.fastmcp.utilities.types import Audio, Image from mcp.server.session import ServerSession +from mcp.server.transport_security import TransportSecuritySettings from mcp.shared.exceptions import McpError from mcp.shared.memory import ( create_connected_server_and_client_session as client_session, @@ -183,6 +184,52 @@ class TestServer: return f"Data: {x}" +class TestDnsRebindingProtection: + """Tests for automatic DNS rebinding protection on localhost.""" + + def test_auto_enabled_for_127_0_0_1(self): + """DNS rebinding protection should auto-enable for host=127.0.0.1.""" + mcp = FastMCP(host="127.0.0.1") + assert mcp.settings.transport_security is not None + assert mcp.settings.transport_security.enable_dns_rebinding_protection is True + assert "127.0.0.1:*" in mcp.settings.transport_security.allowed_hosts + assert "localhost:*" in mcp.settings.transport_security.allowed_hosts + assert "http://127.0.0.1:*" in mcp.settings.transport_security.allowed_origins + assert "http://localhost:*" in mcp.settings.transport_security.allowed_origins + + def test_auto_enabled_for_localhost(self): + """DNS rebinding protection should auto-enable for host=localhost.""" + mcp = FastMCP(host="localhost") + assert mcp.settings.transport_security is not None + assert mcp.settings.transport_security.enable_dns_rebinding_protection is True + assert "127.0.0.1:*" in mcp.settings.transport_security.allowed_hosts + assert "localhost:*" in mcp.settings.transport_security.allowed_hosts + + def test_auto_enabled_for_ipv6_localhost(self): + """DNS rebinding protection should auto-enable for host=::1 (IPv6 localhost).""" + mcp = FastMCP(host="::1") + assert mcp.settings.transport_security is not None + assert mcp.settings.transport_security.enable_dns_rebinding_protection is True + assert "[::1]:*" in mcp.settings.transport_security.allowed_hosts + assert "http://[::1]:*" in mcp.settings.transport_security.allowed_origins + + def test_not_auto_enabled_for_other_hosts(self): + """DNS rebinding protection should NOT auto-enable for other hosts.""" + mcp = FastMCP(host="0.0.0.0") + assert mcp.settings.transport_security is None + + def test_explicit_settings_not_overridden(self): + """Explicit transport_security settings should not be overridden.""" + custom_settings = TransportSecuritySettings( + enable_dns_rebinding_protection=False, + ) + mcp = FastMCP(host="127.0.0.1", transport_security=custom_settings) + # Settings are copied by pydantic, so check values not identity + assert mcp.settings.transport_security is not None + assert mcp.settings.transport_security.enable_dns_rebinding_protection is False + assert mcp.settings.transport_security.allowed_hosts == [] + + def tool_fn(x: int, y: int) -> int: return x + y