mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-23 00:41:40 +00:00
fix(mcp): don't leak resolved internal IP in SSRF error detail
The /token and /register OAuth endpoints are reachable without a
LiteLLM API key — they sit mid-OAuth-handshake. When validate_url
blocked an internal destination, the raw SSRFError ("URL targets a
blocked address (10.0.0.50). …") was wrapped verbatim into the
HTTPException detail and returned to the unauthenticated caller,
handing them the resolved internal address of the operator's IdP —
exactly the reconnaissance the SSRF guard is meant to deny.
Log the real reason at WARNING for operators and return a generic
"the destination resolves to a blocked address" detail to the caller.
Add regression tests asserting the resolved IP does not appear in
the HTTPException detail for either role.
This commit is contained in:
parent
bf935a556c
commit
1332eb4fc3
2 changed files with 23 additions and 3 deletions
|
|
@ -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."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue