diff --git a/src/mcp/server/transport_security.py b/src/mcp/server/transport_security.py index 91b5fa7edb..b9dfdc540f 100644 --- a/src/mcp/server/transport_security.py +++ b/src/mcp/server/transport_security.py @@ -53,17 +53,20 @@ def _validate_host(self, host: str | None) -> bool: logger.warning("Missing Host header in request") return False + host_lower = host.lower() + allowed_hosts_lower = [h.lower() for h in self.settings.allowed_hosts] + # Check exact match first - if host in self.settings.allowed_hosts: + if host_lower in allowed_hosts_lower: return True # Check wildcard port patterns - for allowed in self.settings.allowed_hosts: + for allowed in allowed_hosts_lower: if allowed.endswith(":*"): # Extract base host from pattern base_host = allowed[:-2] # Check if the actual host starts with base host and has a port - if host.startswith(base_host + ":"): + if host_lower.startswith(base_host + ":"): return True logger.warning(f"Invalid Host header: {host}") @@ -75,17 +78,20 @@ def _validate_origin(self, origin: str | None) -> bool: if not origin: return True + origin_lower = origin.lower() + allowed_origins_lower = [o.lower() for o in self.settings.allowed_origins] + # Check exact match first - if origin in self.settings.allowed_origins: + if origin_lower in allowed_origins_lower: return True # Check wildcard port patterns - for allowed in self.settings.allowed_origins: + for allowed in allowed_origins_lower: if allowed.endswith(":*"): # Extract base origin from pattern base_origin = allowed[:-2] # Check if the actual origin starts with base origin and has a port - if origin.startswith(base_origin + ":"): + if origin_lower.startswith(base_origin + ":"): return True logger.warning(f"Invalid Origin header: {origin}") diff --git a/tests/server/test_transport_security.py b/tests/server/test_transport_security.py index 67fe4ef1a1..599281ca19 100644 --- a/tests/server/test_transport_security.py +++ b/tests/server/test_transport_security.py @@ -45,6 +45,12 @@ def _request(host: str | None, origin: str | None, content_type: str | None = "a pytest.param("good.example", "http://evil.example:9000", 403, id="origin-wildcard-base-mismatch"), pytest.param("good.example", "http://good.example", None, id="origin-exact"), pytest.param("good.example", "http://wild.example:9000", None, id="origin-wildcard-match"), + pytest.param("GOOD.EXAMPLE", None, None, id="host-exact-case-insensitive-uppercase-host"), + pytest.param("WILD.EXAMPLE:9000", None, None, id="host-wildcard-case-insensitive-uppercase-host"), + pytest.param("good.example", "HTTP://GOOD.EXAMPLE", None, id="origin-exact-case-insensitive-uppercase-origin"), + pytest.param( + "good.example", "http://WILD.EXAMPLE:9000", None, id="origin-wildcard-case-insensitive-uppercase-origin" + ), ], ) async def test_validate_request_checks_host_then_origin( @@ -56,6 +62,20 @@ async def test_validate_request_checks_host_then_origin( assert (None if response is None else response.status_code) == expected +@pytest.mark.anyio +async def test_validate_request_case_insensitive_uppercase_settings() -> None: + """When allowed_hosts is configured in uppercase (e.g. %COMPUTERNAME% on Windows), lowercase clients pass.""" + settings = TransportSecuritySettings( + enable_dns_rebinding_protection=True, + allowed_hosts=["MYHOST:*", "EXACTHOST"], + allowed_origins=["HTTP://MYHOST:*", "HTTP://EXACTHOST"], + ) + middleware = TransportSecurityMiddleware(settings) + # Lowercase host sent by WHATWG fetch / undici client + assert await middleware.validate_request(_request("myhost:8000", "http://myhost:8000")) is None + assert await middleware.validate_request(_request("exacthost", "http://exacthost")) is None + + @pytest.mark.anyio async def test_validate_request_skips_host_and_origin_when_protection_is_disabled() -> None: """With DNS-rebinding protection off, any Host/Origin is accepted."""