mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
fix(proxy)!: share one destination check between body and path-supplied model
The URL-destination check previously ran over request-body fields only. The per-field logic moves into reject_url_valued_destination(field, value) so a deployment name resolved from the request path runs the same check against the same admin allowlist. BREAKING CHANGE: a deployment name supplied in the request path that parses as an http/https destination is now refused. Add the host to `provider_url_destination_allowed_hosts` in litellm_settings to keep it working.
This commit is contained in:
parent
f047124b5a
commit
fc4be70a37
5 changed files with 77 additions and 21 deletions
|
|
@ -67,7 +67,10 @@ if TYPE_CHECKING:
|
|||
ProxyConfig = _ProxyConfig
|
||||
else:
|
||||
ProxyConfig = Any
|
||||
from litellm.proxy.litellm_pre_call_utils import add_litellm_data_to_request
|
||||
from litellm.proxy.litellm_pre_call_utils import (
|
||||
add_litellm_data_to_request,
|
||||
reject_url_valued_destination,
|
||||
)
|
||||
from litellm.types.utils import (
|
||||
ModelResponse,
|
||||
ModelResponseStream,
|
||||
|
|
@ -1286,6 +1289,9 @@ class ProxyBaseLLMRequestProcessing:
|
|||
self.data[_metadata_variable_name] = {}
|
||||
self.data[_metadata_variable_name]["queue_time_seconds"] = queue_time_seconds
|
||||
|
||||
if isinstance(model, str):
|
||||
reject_url_valued_destination("model", model)
|
||||
|
||||
self.data["model"] = (
|
||||
general_settings.get("completion_model", None) # server default
|
||||
or user_model # model name passed via cli args
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ async def image_generation(
|
|||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
model: str | None = None,
|
||||
):
|
||||
from litellm.proxy.litellm_pre_call_utils import reject_url_valued_destination
|
||||
from litellm.proxy.proxy_server import (
|
||||
add_litellm_data_to_request,
|
||||
general_settings,
|
||||
|
|
@ -96,6 +97,9 @@ async def image_generation(
|
|||
proxy_config=proxy_config,
|
||||
)
|
||||
|
||||
if isinstance(model, str):
|
||||
reject_url_valued_destination("model", model)
|
||||
|
||||
data["model"] = (
|
||||
model
|
||||
or general_settings.get("image_generation_model", None) # server default
|
||||
|
|
|
|||
|
|
@ -262,29 +262,37 @@ def _reject_url_valued_destinations(data: dict[str, Any]) -> None:
|
|||
are unaffected, while admins can opt specific hosts back in via
|
||||
``litellm.provider_url_destination_allowed_hosts``.
|
||||
"""
|
||||
allowed_hosts: Final = getattr(litellm, "provider_url_destination_allowed_hosts", []) or []
|
||||
for field in _URL_DESTINATION_REQUEST_FIELDS:
|
||||
value = data.get(field)
|
||||
if not isinstance(value, str):
|
||||
if isinstance(value, str):
|
||||
reject_url_valued_destination(field, value)
|
||||
|
||||
|
||||
def reject_url_valued_destination(field: str, value: str) -> None:
|
||||
"""Reject a URL-valued destination identifier unless admin-allowlisted.
|
||||
|
||||
Operates on one field/value pair. ``_reject_url_valued_destinations`` applies
|
||||
it across ``_URL_DESTINATION_REQUEST_FIELDS`` for a request body.
|
||||
"""
|
||||
allowed_hosts: Final = getattr(litellm, "provider_url_destination_allowed_hosts", []) or []
|
||||
for candidate in provider_url_destination_candidates(value):
|
||||
if not candidate.lower().startswith(("http://", "https://")):
|
||||
continue
|
||||
for candidate in provider_url_destination_candidates(value):
|
||||
if not candidate.lower().startswith(("http://", "https://")):
|
||||
continue
|
||||
if is_url_destination_allowed_by_host(candidate, allowed_hosts):
|
||||
continue
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "invalid_request",
|
||||
"param": field,
|
||||
"message": (
|
||||
f"URL-valued '{field}' is not allowed. Configure custom "
|
||||
"endpoints with api_base instead, or add the destination "
|
||||
"host to `provider_url_destination_allowed_hosts` in "
|
||||
"litellm_settings."
|
||||
),
|
||||
},
|
||||
)
|
||||
if is_url_destination_allowed_by_host(candidate, allowed_hosts):
|
||||
continue
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={
|
||||
"error": "invalid_request",
|
||||
"param": field,
|
||||
"message": (
|
||||
f"URL-valued '{field}' is not allowed. Configure custom "
|
||||
"endpoints with api_base instead, or add the destination "
|
||||
"host to `provider_url_destination_allowed_hosts` in "
|
||||
"litellm_settings."
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _strip_untrusted_request_header_controls(
|
||||
|
|
|
|||
|
|
@ -120,3 +120,28 @@ def test_azure_image_edit_route(client_no_auth):
|
|||
assert called_kwargs["prompt"] == "A cute baby sea otter"
|
||||
assert response.status_code == 200
|
||||
assert response.json()["data"]
|
||||
|
||||
|
||||
def test_azure_image_generation_route_rejects_url_valued_path_model(client_no_auth):
|
||||
"""A URL-valued deployment segment is refused before any provider call."""
|
||||
client, mock_aimage_generation, _ = client_no_auth
|
||||
response = client.post(
|
||||
"/openai/deployments/oobabooga/https://example.invalid/images/generations",
|
||||
json={"prompt": "A cute baby sea otter", "n": 1, "size": "1024x1024"},
|
||||
)
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "URL-valued" in response.text
|
||||
mock_aimage_generation.assert_not_called()
|
||||
|
||||
|
||||
def test_azure_image_generation_route_allows_ordinary_path_model(client_no_auth):
|
||||
"""A deployment name that merely contains a provider prefix still routes."""
|
||||
client, mock_aimage_generation, _ = client_no_auth
|
||||
response = client.post(
|
||||
"/openai/deployments/dall-e-3/images/generations",
|
||||
json={"prompt": "A cute baby sea otter", "n": 1, "size": "1024x1024"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_aimage_generation.assert_called_once()
|
||||
|
|
|
|||
|
|
@ -177,3 +177,16 @@ async def test_add_litellm_data_to_request_rejects_url_valued_model():
|
|||
)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert exc_info.value.detail["param"] == "model"
|
||||
|
||||
|
||||
class TestNonStringDestinationValues:
|
||||
"""Only string identifiers are inspected. Anything else is left alone for the
|
||||
request's normal validation to handle."""
|
||||
|
||||
@pytest.mark.parametrize("value", [123, None, True, {"a": 1}, ["x"], 1.5])
|
||||
def test_non_string_model_is_ignored(self, value):
|
||||
_reject_url_valued_destinations({"model": value})
|
||||
|
||||
@pytest.mark.parametrize("value", [123, None, True, {"a": 1}, ["x"]])
|
||||
def test_non_string_file_id_is_ignored(self, value):
|
||||
_reject_url_valued_destinations({"file_id": value})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue