fix(guardrails): Alice WonderFence — merge metadata buckets so admin pins can't be shadowed

get_metadata used `metadata or litellm_metadata`, which short-circuits: a
truthy caller-supplied `metadata` bucket meant `litellm_metadata` was never
read. On LITELLM_METADATA_ROUTES (e.g. /v1/responses) proxy-injected admin
pins land in `litellm_metadata` while the caller bucket is `metadata`, so a
caller could shadow the admin pins and fall through to their own
request-metadata override (only exploitable with
allow_request_metadata_override=True — the trusted-gateway case where pinning
must hold).

Merge both buckets with proxy-injected litellm_metadata winning on collision.
Admin pins (nested under user_api_key_metadata / user_api_key_team_metadata)
can no longer be shadowed; the caller's top-level request-override
alice_wonderfence_* keys don't collide and still survive the merge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lior-k 2026-06-04 13:31:15 +03:00
parent 61dafaba12
commit 178fb74fbc
No known key found for this signature in database
2 changed files with 67 additions and 5 deletions

View file

@ -9,6 +9,11 @@ so a caller cannot bypass their assigned WonderFence app.
``allow_request_metadata_override`` defaults to False; enable only for
trusted-gateway deployments that need request-level overrides.
The two metadata buckets (``metadata`` and ``litellm_metadata``) are merged
with the proxy-injected ``litellm_metadata`` winning on key collision, so admin
pins cannot be shadowed by a caller-supplied ``metadata`` body — see
``get_metadata``.
The stash bridges pre_call resolution into post_call where request metadata is
gone — see ``stash_resolved`` for the full rationale.
"""
@ -34,7 +39,19 @@ _LOGGING_OBJ_STASH_KEY = "alice_wonderfence_resolved"
def get_metadata(request_data: dict) -> dict:
return request_data.get("metadata") or request_data.get("litellm_metadata") or {}
"""Merge caller metadata with proxy-injected litellm_metadata.
Proxy-injected values win on key collision so admin-pinned
user_api_key_metadata / user_api_key_team_metadata can never be shadowed
by a caller-supplied `metadata` body. On routes in LITELLM_METADATA_ROUTES
(e.g. /v1/responses) the admin pins live in `litellm_metadata` while the
caller bucket is `metadata`; on /chat/completions they coincide.
"""
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 {}
def resolve_api_key(

View file

@ -207,13 +207,58 @@ def test_resolve_reads_litellm_metadata_when_metadata_absent():
)
def test_get_metadata_prefers_metadata_over_litellm_metadata():
def test_get_metadata_merges_with_litellm_metadata_winning():
"""When both buckets are present, merge them with proxy-injected
``litellm_metadata`` winning on key collision; caller-only keys survive."""
data = {
"metadata": {"alice_wonderfence_app_id": "main"},
"litellm_metadata": {"alice_wonderfence_app_id": "shadow"},
"metadata": {
"alice_wonderfence_app_id": "caller-only",
"shared_key": "from-caller",
},
"litellm_metadata": {
"shared_key": "from-litellm",
"user_api_key_metadata": {"alice_wonderfence_app_id": "admin-pinned"},
},
}
assert get_metadata(data) == {
"alice_wonderfence_app_id": "caller-only",
"shared_key": "from-litellm",
"user_api_key_metadata": {"alice_wonderfence_app_id": "admin-pinned"},
}
assert get_metadata(data) == {"alice_wonderfence_app_id": "main"}
def test_get_metadata_returns_empty_when_both_absent():
assert get_metadata({}) == {}
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
`litellm_metadata.user_api_key_metadata`. The admin pin must win even with
the override flag enabled — the caller bucket must not shadow it."""
data = {
"model": "gpt-4",
"metadata": {"alice_wonderfence_app_id": "caller-override"},
"litellm_metadata": {
"user_api_key_metadata": {"alice_wonderfence_app_id": "admin-pinned"}
},
}
assert resolve_app_id(data, allow_request_metadata_override=True) == "admin-pinned"
def test_responses_route_admin_pin_beats_caller_metadata_api_key():
"""api_key variant of the /v1/responses regression: admin-pinned key
metadata wins over a caller-supplied request-override api_key."""
data = {
"model": "gpt-4",
"metadata": {"alice_wonderfence_api_key": "caller-override"},
"litellm_metadata": {
"user_api_key_metadata": {"alice_wonderfence_api_key": "admin-pinned"}
},
}
assert (
resolve_api_key(
data, default_api_key="default", allow_request_metadata_override=True
)
== "admin-pinned"
)