mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
fix(guardrails): Alice WonderFence get_metadata coerces non-dict metadata buckets
A caller can send `metadata` as a non-object value (string, list). The old
`caller or litellm_md or {}` returned that non-dict verbatim, so resolve_api_key
/ resolve_app_id then called `.get()` on it and raised; with `fail_open=True`
the guardrail swallowed the error and skipped scanning entirely, and the
proxy-injected `litellm_metadata` admin pins were dropped on routes like
/v1/responses where the caller bucket is separate. Coerce each bucket to {}
when it is not a dict before merging, so a malformed caller `metadata` can
neither bypass scanning nor shadow admin-pinned credentials.
Added regression tests (non-dict caller metadata: get_metadata preserves the
litellm_metadata admin pin; resolve_* succeeds from the pin instead of raising).
This commit is contained in:
parent
2e75d23499
commit
b4ab76ecd5
2 changed files with 42 additions and 3 deletions
|
|
@ -49,9 +49,9 @@ def get_metadata(request_data: dict) -> dict:
|
|||
"""
|
||||
caller = request_data.get("metadata")
|
||||
litellm_md = request_data.get("litellm_metadata")
|
||||
if isinstance(caller, dict) and isinstance(litellm_md, dict):
|
||||
return {**caller, **litellm_md}
|
||||
return caller or litellm_md or {}
|
||||
caller = caller if isinstance(caller, dict) else {}
|
||||
litellm_md = litellm_md if isinstance(litellm_md, dict) else {}
|
||||
return {**caller, **litellm_md}
|
||||
|
||||
|
||||
def resolve_api_key(
|
||||
|
|
|
|||
|
|
@ -231,6 +231,45 @@ def test_get_metadata_returns_empty_when_both_absent():
|
|||
assert get_metadata({}) == {}
|
||||
|
||||
|
||||
def test_get_metadata_ignores_non_dict_caller_metadata():
|
||||
"""A caller can send ``metadata`` as a non-object value. It must be coerced
|
||||
away rather than returned verbatim, so the proxy-injected ``litellm_metadata``
|
||||
(carrying the admin pins) is preserved."""
|
||||
data = {
|
||||
"metadata": "not-a-dict",
|
||||
"litellm_metadata": {
|
||||
"user_api_key_metadata": {"alice_wonderfence_app_id": "admin-pinned"}
|
||||
},
|
||||
}
|
||||
assert get_metadata(data) == {
|
||||
"user_api_key_metadata": {"alice_wonderfence_app_id": "admin-pinned"}
|
||||
}
|
||||
|
||||
|
||||
def test_non_dict_caller_metadata_does_not_bypass_resolution():
|
||||
"""Regression: a non-dict ``metadata`` once propagated to ``resolve_*`` and
|
||||
raised on ``.get()``, which ``fail_open=True`` would swallow into a skipped
|
||||
scan. Resolution must instead succeed from the admin pin in
|
||||
``litellm_metadata``."""
|
||||
data = {
|
||||
"model": "gpt-4",
|
||||
"metadata": ["unexpected", "list"],
|
||||
"litellm_metadata": {
|
||||
"user_api_key_metadata": {
|
||||
"alice_wonderfence_app_id": "admin-pinned",
|
||||
"alice_wonderfence_api_key": "admin-key",
|
||||
}
|
||||
},
|
||||
}
|
||||
assert resolve_app_id(data, allow_request_metadata_override=True) == "admin-pinned"
|
||||
assert (
|
||||
resolve_api_key(
|
||||
data, default_api_key=None, allow_request_metadata_override=True
|
||||
)
|
||||
== "admin-key"
|
||||
)
|
||||
|
||||
|
||||
def test_responses_route_admin_pin_beats_caller_metadata():
|
||||
"""Mirror the /v1/responses shape: caller `metadata` carries a
|
||||
request-override app_id while the admin pin lives in
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue