mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
fix(purview): exclude caller-controlled end_user_id from blocking DLP
Blocking Purview checks now use only API-key/JWT-bound user_id, not end_user_id populated from request user/metadata/safety_identifier. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
93e4f4aea9
commit
061b292b2a
3 changed files with 41 additions and 19 deletions
|
|
@ -280,18 +280,25 @@ class PurviewGuardrailBase:
|
|||
) -> Optional[str]:
|
||||
"""Resolve the Entra user object ID from request data or auth context.
|
||||
|
||||
Trust order (strongest first) so client ``metadata[user_id_field]`` cannot
|
||||
impersonate another Entra user for Purview ``protectionScopes`` / ``processContent``:
|
||||
Trust order (strongest first). Blocking DLP uses only
|
||||
``_resolve_trusted_user_id`` (API-key-bound ``user_id``). This resolver
|
||||
also supports audit/logging fallbacks:
|
||||
|
||||
1. ``user_api_key_dict.user_id`` — LiteLLM key / internal user
|
||||
2. ``user_api_key_dict.end_user_id`` — end-user on the API key
|
||||
3. ``metadata["user_api_key_user_id"]`` — proxy-injected from the key (when present)
|
||||
4. ``metadata[user_id_field]`` — caller-supplied; used only when none of the above apply
|
||||
1. ``user_api_key_dict.user_id`` — LiteLLM key / JWT-bound user
|
||||
2. ``user_api_key_dict.end_user_id`` — request-derived; audit only
|
||||
3. ``metadata["user_api_key_user_id"]`` — proxy-injected from the key
|
||||
4. ``metadata[user_id_field]`` — caller-supplied; audit only
|
||||
"""
|
||||
trusted = self._resolve_trusted_user_id(data, user_api_key_dict)
|
||||
if trusted:
|
||||
return trusted
|
||||
|
||||
if (
|
||||
hasattr(user_api_key_dict, "end_user_id")
|
||||
and user_api_key_dict.end_user_id
|
||||
):
|
||||
return str(user_api_key_dict.end_user_id)
|
||||
|
||||
metadata = data.get("metadata") or data.get("litellm_metadata") or {}
|
||||
uid = metadata.get("user_api_key_user_id")
|
||||
if uid:
|
||||
|
|
@ -315,20 +322,23 @@ class PurviewGuardrailBase:
|
|||
def _resolve_trusted_user_id(
|
||||
self, data: Dict[str, Any], user_api_key_dict: Any
|
||||
) -> Optional[str]:
|
||||
"""Resolve user ID from trusted (proxy-authenticated) sources only.
|
||||
"""Resolve user ID from API-key/JWT-bound identity for blocking DLP.
|
||||
|
||||
Uses only ``UserAPIKeyAuth.user_id`` and ``UserAPIKeyAuth.end_user_id``.
|
||||
Intentionally omits ``metadata[user_id_field]`` and
|
||||
``metadata["user_api_key_user_id"]`` because those can be supplied or
|
||||
spoofed by the caller when the API key has no bound user.
|
||||
Uses only ``UserAPIKeyAuth.user_id`` (bound on the LiteLLM key or JWT).
|
||||
Intentionally omits ``UserAPIKeyAuth.end_user_id`` because the proxy sets
|
||||
it from caller-controlled request fields (``user``, ``metadata.user_id``,
|
||||
``safety_identifier``, custom headers, etc.) via
|
||||
``get_end_user_id_from_request_body``.
|
||||
|
||||
Also omits ``metadata[user_id_field]`` and
|
||||
``metadata["user_api_key_user_id"]`` for the same impersonation risk when
|
||||
the key has no bound user.
|
||||
|
||||
Returns ``None`` when no authenticated identity is available. Blocking
|
||||
hooks must fail closed rather than skip the DLP check.
|
||||
"""
|
||||
if hasattr(user_api_key_dict, "user_id") and user_api_key_dict.user_id:
|
||||
return str(user_api_key_dict.user_id)
|
||||
if hasattr(user_api_key_dict, "end_user_id") and user_api_key_dict.end_user_id:
|
||||
return str(user_api_key_dict.end_user_id)
|
||||
|
||||
return None
|
||||
|
||||
|
|
|
|||
|
|
@ -261,11 +261,13 @@ class MicrosoftPurviewDLPGuardrail(PurviewGuardrailBase, CustomGuardrail):
|
|||
"""Resolve user ID for blocking (pre_call / post_call) DLP hooks.
|
||||
|
||||
Uses only trusted proxy-authenticated sources (``_resolve_trusted_user_id``).
|
||||
Caller-supplied ``metadata[user_id_field]`` is rejected (fail closed) because
|
||||
it can impersonate another Entra user's Purview policy.
|
||||
Caller-supplied ``UserAPIKeyAuth.end_user_id`` (from request ``user``,
|
||||
``metadata.user_id``, ``safety_identifier``, etc.) and
|
||||
``metadata[user_id_field]`` are rejected (fail closed) because they can
|
||||
impersonate another Entra user's Purview policy.
|
||||
|
||||
Raises ``HTTPException`` when no trusted identity exists or when only
|
||||
caller-supplied metadata is available (fail closed).
|
||||
Raises ``HTTPException`` when no API-key-bound ``user_id`` exists or when
|
||||
only caller-influenceable identity fields are available (fail closed).
|
||||
"""
|
||||
trusted_id = self._resolve_trusted_user_id(data, user_api_key_dict)
|
||||
if trusted_id:
|
||||
|
|
|
|||
|
|
@ -1803,10 +1803,11 @@ class TestResolveTrustedUserId:
|
|||
auth = UserAPIKeyAuth(api_key="test", user_id="auth-user-111")
|
||||
assert guardrail._resolve_trusted_user_id({}, auth) == "auth-user-111"
|
||||
|
||||
def test_trusted_user_id_from_end_user_id(self):
|
||||
def test_end_user_id_not_trusted_for_blocking(self):
|
||||
"""end_user_id is request-derived; must not be used for blocking DLP."""
|
||||
guardrail = _make_guardrail()
|
||||
auth = UserAPIKeyAuth(api_key="test", end_user_id="end-user-222")
|
||||
assert guardrail._resolve_trusted_user_id({}, auth) == "end-user-222"
|
||||
assert guardrail._resolve_trusted_user_id({}, auth) is None
|
||||
|
||||
def test_metadata_user_api_key_user_id_not_trusted_without_auth(self):
|
||||
"""Metadata user_api_key_user_id is not trusted when the key has no user_id."""
|
||||
|
|
@ -1906,6 +1907,15 @@ class TestResolveUserIdForBlocking:
|
|||
assert exc_info.value.status_code == 400
|
||||
assert "bind user_id" in str(exc_info.value.detail)
|
||||
|
||||
def test_end_user_id_only_raises_for_blocking(self):
|
||||
"""Request-derived end_user_id cannot drive blocking Purview checks."""
|
||||
guardrail = _make_guardrail()
|
||||
auth = UserAPIKeyAuth(api_key="test", end_user_id="caller-end-user")
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
guardrail._resolve_user_id_for_blocking({}, auth)
|
||||
assert exc_info.value.status_code == 400
|
||||
assert "proxy-authenticated" in str(exc_info.value.detail)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Token-id prompt handling in pre_call blocking mode
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue