[Fix] Restrict substring key filters to admin users only

Non-admin users had their auto-set user_id go through substring matching,
which could leak keys belonging to other users with overlapping IDs.
Substring matching for user_id and key_alias now only applies when an
admin explicitly provides the filter parameter.
This commit is contained in:
Ryan Crabbe 2026-03-28 14:20:07 -07:00
parent 33bd570d5e
commit 8881c36405
No known key found for this signature in database

View file

@ -4280,11 +4280,13 @@ async def list_keys(
else:
admin_team_ids = None
use_substring_matching = True
if not user_id and user_api_key_dict.user_role not in [
LitellmUserRoles.PROXY_ADMIN.value,
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value,
]:
user_id = user_api_key_dict.user_id
use_substring_matching = False
response = await _list_key_helper(
prisma_client=prisma_client,
@ -4305,6 +4307,7 @@ async def list_keys(
status=status,
project_id=project_id,
access_group_id=access_group_id,
use_substring_matching=use_substring_matching,
)
verbose_proxy_logger.debug("Successfully prepared response")
@ -4522,6 +4525,7 @@ def _build_key_filter_conditions(
include_created_by_keys: bool = False,
project_id: Optional[str] = None,
access_group_id: Optional[str] = None,
use_substring_matching: bool = False,
) -> Dict[str, Union[str, Dict[str, Any], List[Dict[str, Any]]]]:
"""Build filter conditions for key listing.
@ -4543,15 +4547,21 @@ def _build_key_filter_conditions(
# Base conditions for user's own keys
user_condition: Dict[str, Any] = {}
if user_id and isinstance(user_id, str):
user_condition["user_id"] = {
"contains": user_id,
"mode": "insensitive",
}
if use_substring_matching:
user_condition["user_id"] = {
"contains": user_id,
"mode": "insensitive",
}
else:
user_condition["user_id"] = user_id
if key_alias and isinstance(key_alias, str):
user_condition["key_alias"] = {
"contains": key_alias,
"mode": "insensitive",
}
if use_substring_matching:
user_condition["key_alias"] = {
"contains": key_alias,
"mode": "insensitive",
}
else:
user_condition["key_alias"] = key_alias
if exclude_team_id and isinstance(exclude_team_id, str):
user_condition["team_id"] = {"not": exclude_team_id}
if organization_id and isinstance(organization_id, str):
@ -4654,6 +4664,7 @@ async def _list_key_helper(
status: Optional[str] = None,
project_id: Optional[str] = None,
access_group_id: Optional[str] = None,
use_substring_matching: bool = False,
) -> KeyListResponseObject:
"""
Helper function to list keys
@ -4689,6 +4700,7 @@ async def _list_key_helper(
include_created_by_keys=include_created_by_keys,
project_id=project_id,
access_group_id=access_group_id,
use_substring_matching=use_substring_matching,
)
# Calculate skip for pagination