From e6ac4cb23007d4817b565879d325ee0a042a24be Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 4 Mar 2026 21:27:41 -0800 Subject: [PATCH] Add endpoint-level access control to /v2/user/info Non-admin users can only query their own user_id. Previously relied solely on the auth middleware route check, which proxy admins bypass. This adds defense-in-depth directly in the handler. Co-Authored-By: Claude Opus 4.6 --- .../internal_user_endpoints.py | 13 +++++++ .../test_internal_user_endpoints.py | 34 ++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/internal_user_endpoints.py b/litellm/proxy/management_endpoints/internal_user_endpoints.py index 57d4de4420d..014d0b0c910 100644 --- a/litellm/proxy/management_endpoints/internal_user_endpoints.py +++ b/litellm/proxy/management_endpoints/internal_user_endpoints.py @@ -766,6 +766,19 @@ async def user_info_v2( detail="user_id is required", ) + # Non-admin users can only query their own info + if ( + user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN + and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY + and user_id != user_api_key_dict.user_id + ): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed to access other user's info. Your user_id={}, requested user_id={}".format( + user_api_key_dict.user_id, user_id + ), + ) + user_info = await prisma_client.get_data(user_id=user_id) if user_info is None: diff --git a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py index 4ad0cc8b7cd..e45df19bc45 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py @@ -1508,4 +1508,36 @@ async def test_user_info_v1_has_deprecation_header(mocker): assert result.user_id == "test-user" # Deprecation headers should be set on the response object assert mock_response.headers.get("Deprecation") == "true" - assert "successor-version" in mock_response.headers.get("Link", "") \ No newline at end of file + assert "successor-version" in mock_response.headers.get("Link", "") + + +@pytest.mark.asyncio +async def test_user_info_v2_non_admin_cannot_query_other_user(mocker): + """ + Test that a non-admin user gets 403 when querying another user's info + via /v2/user/info endpoint handler (defense-in-depth). + """ + from fastapi import Request + + from litellm.proxy._types import ProxyException, UserAPIKeyAuth + from litellm.proxy.management_endpoints.internal_user_endpoints import ( + user_info_v2, + ) + + mock_prisma_client = mocker.MagicMock() + mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + mock_request = mocker.MagicMock(spec=Request) + mock_user_api_key_dict = UserAPIKeyAuth( + user_id="user-A", user_role="internal_user" + ) + + with pytest.raises(ProxyException) as exc_info: + await user_info_v2( + user_id="user-B", + user_api_key_dict=mock_user_api_key_dict, + request=mock_request, + ) + + assert exc_info.value.code == "403" + assert "Not allowed" in str(exc_info.value.message) \ No newline at end of file