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.
This commit is contained in:
user 2026-04-17 00:44:24 +00:00
parent c7c3df2b02
commit e3b55794ce
No known key found for this signature in database
2 changed files with 34 additions and 0 deletions

View file

@ -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)

View file

@ -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(