fix(proxy): enforce organization boundaries in admin operations#25904
Conversation
Validate org admin role against all requested organizations instead of returning on first match. Scope team list queries to the caller's permitted organizations when filtering by user_id.
Verify that an org admin of org-A cannot operate on org-B, and that an admin of both orgs can operate on both.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR enforces organization boundaries across multiple admin endpoints by fixing six distinct authorization bypass vulnerabilities: (1) Confidence Score: 5/5Safe to merge — all security fixes are correctly implemented and the primary vulnerability paths are covered by tests. All six authorization bypass fixes are logically correct: the any→all org admin check, batch-fetch org membership before delete_user, silent-create guard, cross-user key update auth, team list org scoping, and the route-level member_delete gate. The only remaining findings are P2: three newly introduced security restrictions (all_users=True, team relocation, org member role protection) lack negative-path tests, but this does not affect correctness or production safety. tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py — missing negative tests for all_users=True and team relocation security gates.
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/auth_checks_organization.py | Fixes _user_is_org_admin to require admin of ALL requested orgs rather than ANY; set comprehension with None-guard is correct. |
| litellm/proxy/management_endpoints/internal_user_endpoints.py | Adds batch org-membership check before delete_user loop (N+1 addressed) and silent-create guard in _update_single_user_helper; logic is correct. |
| litellm/proxy/management_endpoints/team_endpoints.py | Scopes team list to org admin's orgs even when user_id filter is provided; adds destination-org check for team relocation; restricts all_users=True to PROXY_ADMIN. |
| litellm/proxy/management_endpoints/key_management_endpoints.py | Extends key update auth check to all non-budget fields for non-owner callers; preserves owner exemption for non-budget updates. |
| litellm/proxy/management_endpoints/organization_endpoints.py | Adds guard preventing org admins from modifying the per-org role of a global PROXY_ADMIN user. |
| litellm/proxy/_types.py | Adds /organization/member_delete to org_admin_allowed_routes to enforce the same route-level gate as member_add/member_update. |
| tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py | Updates assertion to verify org scoping with user_id filter; formatting refactors; missing negative tests for all_users=True restriction and team relocation guard. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Incoming admin request] --> B{PROXY_ADMIN?}
B -- Yes --> Z[Permitted]
B -- No --> C{Route in org_admin_allowed_routes?}
C -- No --> DENY1[401 Unauthorized]
C -- Yes --> D{_user_is_org_admin ALL orgs?}
D -- No --> DENY2[403 Forbidden]
D -- Yes --> E{Endpoint-specific target check}
E --> E1[delete_user: batch-fetch target memberships]
E --> E2[team list v2: WHERE org IN caller_orgs AND team_id IN user_teams]
E --> E3[key/update: is_key_owner? No = _check_key_admin_access]
E --> E4[user/update: user exists? No + not PROXY_ADMIN = 404]
E --> E5[update_team: org relocate? Must be org-admin of destination]
E --> E6[org member_update: target is PROXY_ADMIN? Only PROXY_ADMIN may modify]
E --> E7[bulk_team_member_add: all_users=True? Only PROXY_ADMIN]
E1 --> Z
E2 --> Z
E3 --> Z
E4 --> Z
E5 --> Z
E6 --> Z
E7 --> Z
Reviews (4): Last reviewed commit: "fix(proxy): close /organization member_d..." | Re-trigger Greptile
Veria admin-queue finding E3NpkuAd, Audit-B #1. The route-level gate accepts this call when the caller is PROXY_ADMIN or ORG_ADMIN of any org named in request_data["organization_id"]/["organizations"]. The handler processes data.user_ids without cross-checking whether those users belong to the caller's administered orgs, so an org-admin of org-A could delete users in org-B via: {"user_ids": ["victim_in_org_B"], "organization_id": "org-A"} Add per-target authorization: org-admins may only delete users whose entire org membership is within their admin scope; targets with any org outside scope (or no org at all) require PROXY_ADMIN. Regression test confirms an ORG_ADMIN call fails with 403 and no cascade delete_many runs.
Greptile P1 on the /user/delete fix. Per CLAUDE.md 'No N+1 queries',
move the find_many inside the per-user loop to a single batched
fetch with {'user_id': {'in': data.user_ids}} before the loop, then
distribute to a per-user set in memory.
Continuation of Veria E3NpkuAd / Audit-B hardening. All three are the same anti-pattern PR BerriAI#25904 already addressed for _user_is_org_admin and /user/delete: route-level gate trusts a caller-supplied scope field, handler operates on a different scope. 1. /user/update no longer silently creates a user when the target email doesn't exist. Pre-fix, an org admin could supply a fresh email + caller-chosen budget/models/metadata; the INSERT path created the user with no org attachment, bypassing /user/new's org/team authorization. Now require PROXY_ADMIN for the create branch; return 404 otherwise. Also fixes /user/bulk_update because it dispatches through the same _update_single_user_helper. 2. /team/bulk_member_add with all_users=true restricted to PROXY_ADMIN. The flag pulls every user in the database into the target team, ignoring org scope — any team admin could use it to capture every user across every org into their team. 3. /team/update now verifies destination-org admin rights. When the request carries an organization_id that differs from the team's current org, an org admin of the caller's current org could previously relocate the team into any other org (draining their resources, or capturing a team they once administered). Require PROXY_ADMIN or org-admin of the DESTINATION org for the relocation. Regression tests for #1 and BerriAI#3; BerriAI#2 covered by the existing bulk_add suite after the gate addition.
Audit-B BerriAI#2. _check_key_admin_access was gated on max_budget/spend changes only, which meant a non-admin caller could blanket-rewrite any OTHER field on any key (key_alias, models, tpm_limit, rpm_limit, metadata, tags, allowed_routes, guardrails, blocked, duration, permissions, auto_rotate, access_group_ids, object_permission, …) as long as they avoided budget/spend. Example attack: POST /key/update { key: sk-victim-in-org-B, models: [], blocked: true, organization_id: org-A, } The caller is org-admin of org-A, which satisfies the route gate; the handler then wipes models and blocks the victim's key. Policy after this fix: - PROXY_ADMIN: always allowed. - Key OWNER (matching user_id): allowed for non-budget fields; budget/spend changes still require team/org admin. - Everyone else: must pass _check_key_admin_access (PROXY_ADMIN / key-owner / team-admin / org-admin of the key). Regression test confirms a non-owner INTERNAL_USER cannot rewrite key_alias/blocked on someone else's key; existing test covers the owner-can-update-alias case; existing test covers internal-user-cannot-modify-max-budget.
Audit-B BerriAI#7 and BerriAI#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.
50a6324
into
BerriAI:litellm_yj_apr17
Relevant issues
Fixes inconsistent organization boundary checks in team listing and user creation flows.
Reopens #25829 (original base
litellm_yj_apr15was deleted); rebased ontolitellm_internal_staging. No content changes.Pre-Submission checklist
tests/test_litellm/directory, Adding at least 1 test is a hard requirement - see detailsmake test-unit@greptileaiand received a Confidence Score of at least 4/5 before requesting a maintainer reviewType
🐛 Bug Fix
Changes
1. Validate org admin role against all requested organizations
_user_is_org_admin()previously returnedTrueif the caller was admin of any one of the listed organizations. Now validates against all of them — an org admin of org-A cannot perform operations targeting org-B.2. Scope team list queries to caller's organizations
_build_team_list_where_conditions()previously dropped the organization filter when auser_idparameter was provided, returning teams across all organizations. Now always applies the org filter for org admins._authorize_and_filter_teams()(legacy path) previously fetched teams outside the caller's orgs if the target user was a member. Now only returns teams within the caller's permitted organizations.