From f9cd054445998dfcc5bd9bad74f5d50a50654096 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sun, 31 May 2026 06:37:44 +0000 Subject: [PATCH] chore(proxy): extend SSRF/destination guards to provider endpoint overrides The URL validation and destination-change credential clearing only covered api_base/base_url, but provider-specific endpoint URLs (aws_bedrock_runtime_endpoint, aws_sts_endpoint) are equally caller-controlled: a non-admin team model could point one at an internal/metadata address while reusing stored AWS credentials. Validate those endpoints too and treat them as destination overrides (model management + connection-test guard). Also make the os.environ/ reference scan iterative (stack-based) instead of recursive to satisfy the recursive-function check. --- .../health_endpoints/_health_endpoints.py | 2 ++ .../model_management_endpoints.py | 32 +++++++++++++------ .../test_model_management_endpoints.py | 5 +++ 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/litellm/proxy/health_endpoints/_health_endpoints.py b/litellm/proxy/health_endpoints/_health_endpoints.py index 2b9a4b07581..6b35f41deb5 100644 --- a/litellm/proxy/health_endpoints/_health_endpoints.py +++ b/litellm/proxy/health_endpoints/_health_endpoints.py @@ -88,6 +88,8 @@ _HEALTH_CREDENTIAL_FIELDS = ( _HEALTH_DESTINATION_FIELDS = ( "api_base", "base_url", + "aws_bedrock_runtime_endpoint", + "aws_sts_endpoint", "api_version", "vertex_location", "vertex_project", diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index ed5a3ea92f8..ccb49bf7fcd 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -77,7 +77,16 @@ _CREDENTIAL_LITELLM_PARAMS = ( "aws_session_token", "vertex_credentials", ) -_DESTINATION_LITELLM_PARAMS = ("api_base", "base_url", "custom_llm_provider") +# Caller-controlled endpoint URLs that must be SSRF-validated on write. +_URL_LITELLM_PARAMS = ( + "api_base", + "base_url", + "aws_bedrock_runtime_endpoint", + "aws_sts_endpoint", +) +# Destination fields whose change must drop an inherited credential (URLs above +# plus the provider selector, which isn't a URL so isn't SSRF-validated). +_DESTINATION_LITELLM_PARAMS = _URL_LITELLM_PARAMS + ("custom_llm_provider",) def _field_explicitly_set(model: Optional[BaseModel], field: str) -> bool: @@ -92,7 +101,7 @@ def _validate_model_url_params(litellm_params: dict) -> None: with user_url_allowed_hosts as the escape hatch for internal endpoints.""" if not getattr(litellm, "user_url_validation", False): return - for url_field in ("api_base", "base_url"): + for url_field in _URL_LITELLM_PARAMS: url_value = litellm_params.get(url_field) if not url_value or not isinstance(url_value, str): continue @@ -151,13 +160,18 @@ def _is_pricing_field(field: str) -> bool: def _contains_env_reference(value: object) -> bool: """True if any (possibly nested) string is an `os.environ/` reference, which - resolves to a server-side environment secret at call time.""" - if isinstance(value, str): - return value.startswith("os.environ/") - if isinstance(value, dict): - return any(_contains_env_reference(v) for v in value.values()) - if isinstance(value, list): - return any(_contains_env_reference(v) for v in value) + resolves to a server-side environment secret at call time. Iterative (stack) + rather than recursive, matching _reject_os_environ_references.""" + stack: List[object] = [value] + while stack: + item = stack.pop() + if isinstance(item, str): + if item.startswith("os.environ/"): + return True + elif isinstance(item, dict): + stack.extend(item.values()) + elif isinstance(item, list): + stack.extend(item) return False diff --git a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py index 8b7cde9e166..f3d3a8a8b9b 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_model_management_endpoints.py @@ -2044,6 +2044,11 @@ class TestModelMgmtAuthzHardening: _validate_model_url_params( {"api_base": "http://169.254.169.254/latest/meta-data/"} ) + # Provider-specific endpoint URLs are guarded too, not just api_base. + with pytest.raises(ProxyException): + _validate_model_url_params( + {"aws_bedrock_runtime_endpoint": "http://169.254.169.254/"} + ) # Honors the opt-out toggle (internal endpoints / Ollama). with ( patch.object(litellm, "user_url_validation", False),