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:
lior-k 2026-06-08 18:21:54 +03:00
parent 2e75d23499
commit b4ab76ecd5
No known key found for this signature in database
2 changed files with 42 additions and 3 deletions

View file

@ -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(

View file

@ -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