From 8881c364055e4caeedaf45fcaae642cc242cf96a Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Sat, 28 Mar 2026 14:20:07 -0700 Subject: [PATCH] [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. --- .../key_management_endpoints.py | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index d82febea8fe..5452f75e26b 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -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