diff --git a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py index e27589ff567..59707f240b9 100644 --- a/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py +++ b/litellm/proxy/_experimental/mcp_server/discoverable_endpoints.py @@ -46,9 +46,21 @@ def _validate_mcp_oauth_outbound_url( try: return validate_url(url) except SSRFError as exc: + # The /token and /register endpoints are reachable without an API key, + # so the error response goes to an unauthenticated caller. The raw + # SSRFError message includes the resolved IP — leaking it would tell + # the caller exactly which internal address the operator's IdP lives at, + # which is the reconnaissance the SSRF guard is meant to deny. Log the + # real reason for operators and return a generic message to the caller. + verbose_logger.warning( + "MCP OAuth %s URL blocked by SSRF validation: %s", role, exc + ) raise HTTPException( status_code=400, - detail=f"Configured MCP {role} URL is not safe to call: {exc}", + detail=( + f"Configured MCP {role} URL is not safe to call: " + "the destination resolves to a blocked address." + ), ) diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py index 06926ecf9eb..6e4c2305e1c 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/test_discoverable_endpoints.py @@ -2537,17 +2537,25 @@ class TestOutboundOAuthURLValidation: "http://127.0.0.1:8080/token", role="token" ) assert exc_info.value.status_code == 400 - assert "token" in str(exc_info.value.detail) + detail = str(exc_info.value.detail) + assert "token" in detail + # The /token endpoint is unauthenticated — leaking the resolved IP + # would hand reconnaissance to the caller. Detail must stay generic. + assert "127.0.0.1" not in detail + assert "blocked address" in detail def test_validator_rejects_rfc1918_url(self): from litellm.proxy._experimental.mcp_server.discoverable_endpoints import ( _validate_mcp_oauth_outbound_url, ) - with pytest.raises(HTTPException): + with pytest.raises(HTTPException) as exc_info: _validate_mcp_oauth_outbound_url( "http://192.168.1.10/oauth/register", role="registration" ) + detail = str(exc_info.value.detail) + assert "192.168.1.10" not in detail + assert "registration" in detail def test_validator_passes_when_validation_disabled(self): # Operators who explicitly opt out via litellm.user_url_validation = False