fix(ssrf): point the blocked-address remediation at litellm_settings (#42508)

* fix(ssrf): point the blocked-address remediation at litellm_settings

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(ssrf): pin the block message's named section to litellm_settings

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(compaction): pin litellm.max_budget so leaked proxy budget cannot 401 the child auth

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: shivam <shivam@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-22 14:04:16 -07:00 committed by GitHub
parent b73696a15d
commit 0e6a288458
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 33 additions and 3 deletions

View file

@ -82,7 +82,7 @@ def _rejected_image_fetch(url: str, verdict: SSRFError) -> "litellm.ImageFetchEr
verbose_logger.warning("Image fetch of %s rejected before any request went out: %s", url, verdict)
return litellm.ImageFetchError(
"Error: Unable to fetch image from URL. The proxy could not resolve this host or its URL policy rejected it; "
f"an admin can check the proxy log and `user_url_allowed_hosts` in general_settings. url={url}"
f"an admin can check the proxy log and `user_url_allowed_hosts` in litellm_settings. url={url}"
)

View file

@ -336,7 +336,7 @@ def validate_url(url: str) -> tuple[str, str]:
raise SSRFError(
f"URL targets a blocked address ({resolved_ip}). "
"If this is a legitimate internal service, add the host "
"to `user_url_allowed_hosts` in general_settings."
"to `user_url_allowed_hosts` in litellm_settings."
)
# For HTTPS with SSL verification enabled, TLS certificate validation

View file

@ -427,7 +427,7 @@ async def test_async_inline_remote_media_cancels_the_other_fetches_when_one_fail
_SSRF_VERDICTS = (
SSRFError(
"URL targets a blocked address (10.0.0.8). If this is a legitimate internal service, "
"add the host to `user_url_allowed_hosts` in general_settings."
"add the host to `user_url_allowed_hosts` in litellm_settings."
),
SSRFError("DNS resolution failed for 'internal.example': [Errno 8] nodename nor servname provided, or not known"),
SSRFError("No addresses found for 'internal.example'"),

View file

@ -2198,6 +2198,34 @@ async def test_ProxyConfig_load_config_wires_general_settings_url_validation(tmp
litellm.provider_url_destination_allowed_hosts = original_provider_hosts
@pytest.mark.asyncio
async def test_ssrf_block_message_names_a_config_section_load_config_honors(tmp_path, monkeypatch):
"""Regression for LIT-8349: the remediation in the SSRF block message must point at a section that works."""
from litellm.litellm_core_utils.url_utils import SSRFError, validate_url
monkeypatch.setattr(litellm, "user_url_allowed_hosts", [])
monkeypatch.setattr(litellm, "user_url_validation", True)
with pytest.raises(SSRFError) as blocked:
validate_url("http://10.96.3.245:10002/agent.json")
section_match = re.search(r"add the host to `user_url_allowed_hosts` in (\w+)\.", str(blocked.value))
assert section_match is not None, str(blocked.value)
section: Final = section_match.group(1)
assert section == "litellm_settings", f"block message points admins at {section}, which the docs contradict"
f = tmp_path / "c.yaml"
f.write_text(f"model_list: []\n{section}:\n user_url_allowed_hosts:\n - '10.96.3.245:10002'\n")
monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", None)
monkeypatch.setattr("litellm.proxy.proxy_server.store_model_in_db", False)
monkeypatch.delenv("LITELLM_CONFIG_BUCKET_NAME", raising=False)
await ProxyConfig().load_config(router=None, config_file_path=str(f))
assert litellm.user_url_allowed_hosts == ["10.96.3.245:10002"], f"{section} did not apply the allowlist"
assert validate_url("http://10.96.3.245:10002/agent.json") == (
"http://10.96.3.245:10002/agent.json",
"10.96.3.245:10002",
)
@pytest.mark.asyncio
async def test_ProxyConfig_load_config_wires_config_reload_interval(tmp_path, monkeypatch):
"""general_settings.proxy_config_reload_interval_seconds must reach the proxy_server

View file

@ -7,6 +7,7 @@ import pytest
from fastapi import FastAPI, Request
from pydantic import TypeAdapter
import litellm
from litellm.caching.caching import DualCache
from litellm.exceptions import BadRequestError
from litellm.litellm_core_utils.initialize_dynamic_callback_params import inherit_message_logging_privacy
@ -112,6 +113,7 @@ async def test_real_proxy_child_auth_privacy_and_body_policy(
})))
return asyncio.sleep(0, result=ModelResponse(id="private-summary", model="compactor"))
monkeypatch.setattr(litellm, "max_budget", 0)
monkeypatch.setattr(proxy_server.app, "dependency_overrides", {})
monkeypatch.setattr(proxy_server, "master_key", "sk-master-fixture")
monkeypatch.setattr(proxy_server, "prisma_client", object())