From e3b55794ce14e26c8d39b469a5388be4d2f4bc34 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:44:24 +0000 Subject: [PATCH] fix(proxy): close /organization member_delete + role-escalation gaps Audit-B #7 and #8. 1. /organization/member_delete was not in org_admin_only_routes, so it fell through to management_routes/self_managed_routes and let any caller that reached the route delete arbitrary org memberships without the organization_role_based_access_check that member_add and member_update trigger. Adding it to org_admin_only_routes applies the same ORG_ADMIN-of-target-org gate. 2. /organization/member_update had no validation that the target user was not a global PROXY_ADMIN. An org-admin of any org could alter a PROXY_ADMIN user's per-org role. Reject this unless the caller is PROXY_ADMIN. --- litellm/proxy/_types.py | 7 +++++ .../organization_endpoints.py | 27 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 7fff640c497..39857984247 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -690,6 +690,13 @@ class LiteLLMRoutes(enum.Enum): "/organization/delete", "/organization/member_add", "/organization/member_update", + # member_delete is equally destructive as member_add / member_update + # and must be scoped the same way — otherwise it falls through to + # the management_routes / self_managed_routes path and lets any + # non-PROXY_ADMIN caller that reaches the route delete arbitrary + # org memberships without the organization_role_based_access_check + # that member_add / member_update trigger. + "/organization/member_delete", ] # Routes accessible by Admin Viewer (read-only admin access) diff --git a/litellm/proxy/management_endpoints/organization_endpoints.py b/litellm/proxy/management_endpoints/organization_endpoints.py index 25df9f0b0f7..670946ac0d8 100644 --- a/litellm/proxy/management_endpoints/organization_endpoints.py +++ b/litellm/proxy/management_endpoints/organization_endpoints.py @@ -1064,6 +1064,33 @@ async def organization_member_update( }, ) + # Reject attempts to change the role of a global PROXY_ADMIN via + # org-scoped operations. An org-admin of any org could otherwise + # alter a PROXY_ADMIN user's per-org role, which has downstream + # effects on admin UI filtering and scope derivation. + target_user_row = await prisma_client.db.litellm_usertable.find_unique( + where={"user_id": data.user_id} + ) + if target_user_row is not None and getattr( + target_user_row, "user_role", None + ) in ( + LitellmUserRoles.PROXY_ADMIN.value, + LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY.value, + ): + if ( + user_api_key_dict.user_role + != LitellmUserRoles.PROXY_ADMIN.value + ): + raise HTTPException( + status_code=403, + detail={ + "error": ( + "Only PROXY_ADMIN may modify the organization " + "role of a user who is a global PROXY_ADMIN." + ) + }, + ) + # Update member role if data.role is not None: await prisma_client.db.litellm_organizationmembership.update(