From 3bf57ba6b941fa1037fa8581a6b102dc29bd97c0 Mon Sep 17 00:00:00 2001 From: "cursor-cloud[bot]" Date: Thu, 7 May 2026 21:45:54 +0000 Subject: [PATCH] fix(proxy): wrap bare-string user_url_allowed_hosts as one-element list Address greptile review feedback on PR #27034. list("internal.corp") splits a string into individual characters, so an operator following the SSRFError hint who writes a bare string instead of a YAML list: user_url_allowed_hosts: internal.corp would silently end up with ["i", "n", "t", ...], none of which match a real hostname. Wrap a bare string as a one-element list so the operator's intent is preserved. Adds a regression test. Co-authored-by: Mateo Wang --- litellm/proxy/proxy_server.py | 5 +- .../test_proxy_config_unit_test.py | 49 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 40c66d5894f..664060ff9ad 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -4074,7 +4074,10 @@ class ProxyConfig: "user_url_allowed_hosts", None ) if _user_url_allowed_hosts is not None: - litellm.user_url_allowed_hosts = list(_user_url_allowed_hosts) + if isinstance(_user_url_allowed_hosts, str): + litellm.user_url_allowed_hosts = [_user_url_allowed_hosts] + else: + litellm.user_url_allowed_hosts = list(_user_url_allowed_hosts) ## BUDGET RESCHEDULER ## proxy_budget_rescheduler_min_time = general_settings.get( "proxy_budget_rescheduler_min_time", proxy_budget_rescheduler_min_time diff --git a/tests/proxy_unit_tests/test_proxy_config_unit_test.py b/tests/proxy_unit_tests/test_proxy_config_unit_test.py index 08683c79eb1..350fd376e0c 100644 --- a/tests/proxy_unit_tests/test_proxy_config_unit_test.py +++ b/tests/proxy_unit_tests/test_proxy_config_unit_test.py @@ -434,3 +434,52 @@ async def test_general_settings_url_validation_string_value(raw_value, expected) finally: os.unlink(temp_file_path) litellm.user_url_validation = original_validation + + +@pytest.mark.asyncio +async def test_general_settings_url_allowed_hosts_bare_string(): + """ + A bare string value for user_url_allowed_hosts in YAML must be wrapped in + a one-element list rather than passed through list(...), which would split + it into characters. Operators following the SSRFError hint may write + `user_url_allowed_hosts: internal.corp` and reasonably expect that single + host to be allowlisted. + """ + import tempfile + + import yaml + + config_content = { + "model_list": [ + { + "model_name": "test-model", + "litellm_params": {"model": "openai/gpt-4", "api_key": "test-key"}, + } + ], + "general_settings": { + "user_url_allowed_hosts": "internal.corp", + }, + } + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".yaml", delete=False + ) as temp_file: + yaml.dump(config_content, temp_file) + temp_file_path = temp_file.name + + original_hosts = litellm.user_url_allowed_hosts + + try: + proxy_config = ProxyConfig() + await proxy_config.load_config( + router=None, + config_file_path=temp_file_path, + ) + + assert litellm.user_url_allowed_hosts == ["internal.corp"], ( + "Bare-string user_url_allowed_hosts must be wrapped as a one-element " + f"list, not split into characters. Got {litellm.user_url_allowed_hosts!r}" + ) + finally: + os.unlink(temp_file_path) + litellm.user_url_allowed_hosts = original_hosts