From 0e6a288458640f4499e646c1d0ad445c14a85bfb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:04:16 -0700 Subject: [PATCH] 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 Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../prompt_templates/image_handling.py | 2 +- litellm/litellm_core_utils/url_utils.py | 2 +- .../litellm_core_utils/test_image_handling.py | 2 +- .../proxy/proxy_server/test_proxy_config.py | 28 +++++++++++++++++++ .../proxy/test_native_compaction.py | 2 ++ 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/litellm/litellm_core_utils/prompt_templates/image_handling.py b/litellm/litellm_core_utils/prompt_templates/image_handling.py index c9933422cc3..c44c80bc0a0 100644 --- a/litellm/litellm_core_utils/prompt_templates/image_handling.py +++ b/litellm/litellm_core_utils/prompt_templates/image_handling.py @@ -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}" ) diff --git a/litellm/litellm_core_utils/url_utils.py b/litellm/litellm_core_utils/url_utils.py index b94d5a6886d..6c87ef4a3de 100644 --- a/litellm/litellm_core_utils/url_utils.py +++ b/litellm/litellm_core_utils/url_utils.py @@ -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 diff --git a/tests/test_litellm/litellm_core_utils/test_image_handling.py b/tests/test_litellm/litellm_core_utils/test_image_handling.py index 8fa4bd6c14d..21e97e97357 100644 --- a/tests/test_litellm/litellm_core_utils/test_image_handling.py +++ b/tests/test_litellm/litellm_core_utils/test_image_handling.py @@ -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'"), diff --git a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py index 832ac6402d9..b47cce43dcc 100644 --- a/tests/test_litellm/proxy/proxy_server/test_proxy_config.py +++ b/tests/test_litellm/proxy/proxy_server/test_proxy_config.py @@ -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 diff --git a/tests/test_litellm/proxy/test_native_compaction.py b/tests/test_litellm/proxy/test_native_compaction.py index d24624aacc5..9a6b4cdda65 100644 --- a/tests/test_litellm/proxy/test_native_compaction.py +++ b/tests/test_litellm/proxy/test_native_compaction.py @@ -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())