mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
fix(proxy)!: parse bracket-notation form metadata the same way its JSON form is parsed
Multipart callers express nested metadata as flat bracket-notation keys, which
reach the request-body check as literal keys rather than as a metadata dict.
The check now rebuilds them with the same helper the endpoints use, so both
encodings are handled identically and cannot drift apart.
BREAKING CHANGE: a multipart field such as `litellm_metadata[api_base]` is now
subject to the same request-body parameter rules as its JSON equivalent. Set
`general_settings.allow_client_side_credentials`, or the deployment's
`configurable_clientside_auth_params`, to keep passing these.
(cherry picked from commit 5b2c92d749)
This commit is contained in:
parent
f916951058
commit
dde20e405c
2 changed files with 85 additions and 0 deletions
|
|
@ -19,6 +19,7 @@ from litellm.litellm_core_utils.url_utils import (
|
|||
validate_url,
|
||||
)
|
||||
from litellm.proxy._types import *
|
||||
from litellm.proxy.common_utils.http_parsing_utils import extract_nested_form_metadata
|
||||
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
|
||||
LITELLM_PASS_THROUGH_ENDPOINT_MARKER,
|
||||
)
|
||||
|
|
@ -439,6 +440,13 @@ def is_request_body_safe(request_body: dict, general_settings: dict, llm_router:
|
|||
metadata = _coerce_metadata_to_dict(request_body.get(metadata_key))
|
||||
if metadata is not None:
|
||||
_check_banned_params(metadata, general_settings, llm_router, model)
|
||||
if any(isinstance(key, str) and key.startswith(f"{metadata_key}[") for key in request_body):
|
||||
_check_banned_params(
|
||||
extract_nested_form_metadata(form_data=request_body, prefix=f"{metadata_key}["),
|
||||
general_settings,
|
||||
llm_router,
|
||||
model,
|
||||
)
|
||||
for target in iter_request_fallback_targets(request_body):
|
||||
if isinstance(target, dict):
|
||||
_check_banned_params(target, general_settings, llm_router, model)
|
||||
|
|
|
|||
|
|
@ -2913,3 +2913,80 @@ class TestGetKeyTagRateLimits:
|
|||
def test_returns_none_when_unset(self):
|
||||
key = UserAPIKeyAuth(api_key="sk-123")
|
||||
assert get_key_tag_rpm_limit(key) is None
|
||||
|
||||
|
||||
class TestIsRequestBodySafeChecksBracketNotationMetadata:
|
||||
"""Bracket notation is how multipart callers express nested metadata; it is
|
||||
validated the same way the dict form is."""
|
||||
|
||||
@pytest.mark.parametrize("metadata_key", ["metadata", "litellm_metadata"])
|
||||
def test_bracket_notation_banned_param_is_rejected(self, metadata_key):
|
||||
with pytest.raises(ValueError, match="langfuse_host"):
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"purpose": "assistants",
|
||||
f"{metadata_key}[langfuse_host]": "https://example.invalid",
|
||||
},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
def test_bracket_notation_api_base_is_rejected(self):
|
||||
with pytest.raises(ValueError, match="api_base"):
|
||||
is_request_body_safe(
|
||||
request_body={"litellm_metadata[api_base]": "https://example.invalid"},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
|
||||
def test_bracket_notation_allowed_under_proxy_wide_opt_in(self):
|
||||
assert (
|
||||
is_request_body_safe(
|
||||
request_body={"litellm_metadata[langfuse_host]": "https://byok.example"},
|
||||
general_settings={"allow_client_side_credentials": True},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_benign_bracket_notation_metadata_is_allowed(self):
|
||||
assert (
|
||||
is_request_body_safe(
|
||||
request_body={
|
||||
"purpose": "assistants",
|
||||
"litellm_metadata[spend_logs_metadata][owner]": "john",
|
||||
"litellm_metadata[tags]": "production",
|
||||
},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_bracket_notation_matches_json_encoding_for_deeper_nesting(self):
|
||||
"""A value nested below the first level is treated the same either way:
|
||||
the check descends one level into metadata, for both encodings."""
|
||||
deep_bracket = {
|
||||
"litellm_metadata[spend_logs_metadata][langfuse_host]": "https://example.invalid"
|
||||
}
|
||||
deep_json = {
|
||||
"litellm_metadata": {"spend_logs_metadata": {"langfuse_host": "https://example.invalid"}}
|
||||
}
|
||||
kwargs = dict(general_settings={}, llm_router=None, model="gpt-4")
|
||||
assert is_request_body_safe(request_body=deep_bracket, **kwargs) is True
|
||||
assert is_request_body_safe(request_body=deep_json, **kwargs) is True
|
||||
|
||||
def test_body_without_bracket_keys_is_unaffected(self):
|
||||
assert (
|
||||
is_request_body_safe(
|
||||
request_body={"model": "gpt-4", "messages": [{"role": "user", "content": "hi"}]},
|
||||
general_settings={},
|
||||
llm_router=None,
|
||||
model="gpt-4",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue