Merge commit from fork
Some checks failed
Main branch checks / checks (push) Has been cancelled

* Auto-enable DNS rebinding protection for localhost servers

When a FastMCP server is created with host="127.0.0.1" or "localhost"
and no explicit transport_security is provided, automatically enable
DNS rebinding protection. Both 127.0.0.1 and localhost are allowed
as valid hosts/origins since clients may use either to connect.

* Add tests for auto DNS rebinding protection on localhost

Tests verify that:
- Protection auto-enables for host=127.0.0.1
- Protection auto-enables for host=localhost
- Both 127.0.0.1 and localhost are in allowed hosts/origins
- Protection does NOT auto-enable for other hosts (e.g., 0.0.0.0)
- Explicit transport_security settings are not overridden

* Add IPv6 localhost (::1) support for DNS rebinding protection

Extend auto-enable DNS rebinding protection to also cover IPv6
localhost. When host="::1", protection is now auto-enabled with
appropriate allowed hosts ([::1]:*) and origins (http://[::1]:*).

* Fix import ordering in test file
This commit is contained in:
Paul Carleton 2025-12-02 13:23:55 +00:00 • committed by GitHub
commit d3a184119e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 0 deletions

View file

@ -174,6 +174,14 @@ class FastMCP(Generic[LifespanResultT]):
auth: AuthSettings | None = None, auth: AuthSettings | None = None,
transport_security: TransportSecuritySettings | 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( self.settings = Settings(
debug=debug, debug=debug,
log_level=log_level, log_level=log_level,

View file

@ -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.resources import FileResource, FunctionResource
from mcp.server.fastmcp.utilities.types import Audio, Image from mcp.server.fastmcp.utilities.types import Audio, Image
from mcp.server.session import ServerSession from mcp.server.session import ServerSession
from mcp.server.transport_security import TransportSecuritySettings
from mcp.shared.exceptions import McpError from mcp.shared.exceptions import McpError
from mcp.shared.memory import ( from mcp.shared.memory import (
create_connected_server_and_client_session as client_session, create_connected_server_and_client_session as client_session,
@ -183,6 +184,52 @@ class TestServer:
return f"Data: {x}" 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: def tool_fn(x: int, y: int) -> int:
return x + y return x + y