fix: tighten budget field validation and authorization checks#27897
Conversation
|
|
Congrats! CodSpeed is installed 🎉
You will start to see performance impacts in the reports once the benchmarks are run from your default branch.
|
Addresses three security issues:
GHSA-wvg4-6222-3q4r: /user/update exposes max_budget, soft_budget, spend
to self-editing non-admin users with no server-side guard. Non-admin callers
now receive HTTP 403 if any of those fields appear in the update payload.
GHSA-q775-qw9r-2r4g: _enforce_upperbound_key_params returned early (no-op)
when upperbound_key_generate_params was absent from config, letting any
authenticated user generate a key with unlimited max_budget. Fix adds a
delegated-authority ceiling in _common_key_generation_helper: non-admins
cannot grant a key more budget than their own key carries.
GHSA-2rv4-xv66-fpjg: float('nan') passes every `value < 0` guard because
nan < 0 is False in Python, and spend >= nan is always False, permanently
disabling budget enforcement for any entity carrying a NaN max_budget.
All write-time budget guards now use `not math.isfinite(v) or v < 0`.
_enforce_upperbound_key_params validates finiteness unconditionally (before
the early-return). All spend-enforcement comparisons in auth_checks.py are
now guarded with math.isfinite(max_budget) as defense-in-depth.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
9105db7 to
d0368bc
Compare
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThis PR hardens budget enforcement across the LiteLLM proxy by (1) adding
Confidence Score: 3/5The NaN/Inf validation and GHSA fixes are mechanically correct, but the authorization changes for user-update and key-update paths are backwards-incompatible for deployments using org-admin or team-admin roles to manage budgets, and these concerns remain open from the prior review round. The core NaN/Inf defense and delegated-authority ceiling are correctly implemented. However, the self-escalation guard in _update_single_user_helper blocks every non-PROXY_ADMIN role including org admins and team admins from writing budget fields on records they previously managed. Deployments relying on those admin tiers will start receiving 403s with no opt-out path. These backwards-incompatible changes have not been gated behind a feature flag as the repository rule for breaking changes requires. internal_user_endpoints.py (self-escalation guard role coverage) and key_management_endpoints.py (delegated-authority ceiling and key-update authorization) carry the highest deployment risk.
|
| Filename | Overview |
|---|---|
| litellm/proxy/auth/auth_checks.py | Adds math.isfinite() guard before every budget comparison to prevent NaN-based enforcement bypass; consistent and correct across all callsites. |
| litellm/proxy/management_endpoints/budget_management_endpoints.py | Extends max_budget/soft_budget validation from not-negative to finite-and-not-negative in both /budget/new and /budget/update; error messages updated consistently. |
| litellm/proxy/management_endpoints/internal_user_endpoints.py | Adds GHSA-wvg4 self-escalation guard blocking non-PROXY_ADMIN callers from writing max_budget/soft_budget/spend on their own record; also blocks ORG_ADMIN/TEAM_ADMIN roles from budget management (already flagged in previous review rounds). |
| litellm/proxy/management_endpoints/key_management_endpoints.py | Adds NaN/Inf validation in _enforce_upperbound_key_params; adds GHSA-q775 delegated-authority ceiling with _requested_max_budget correctly captured before default-param fill-in. Key-update budget path protected via _check_key_admin_access. Backwards-compatibility concerns from prior rounds remain open. |
| litellm/proxy/management_endpoints/organization_endpoints.py | Extends org create/update budget validation to include math.isfinite() check. |
| litellm/proxy/management_endpoints/team_endpoints.py | Extends team create/update budget validation to include math.isfinite() guard; symmetric with other endpoint files. |
| tests/test_litellm/proxy/management_endpoints/test_budget_endpoints.py | Four existing tests updated to match revised error message; assertions tightened, not weakened. |
| tests/test_litellm/proxy/management_endpoints/test_internal_user_endpoints.py | Adds three regression tests for GHSA-wvg4: non-admin self-escalation blocked, spend reset blocked, PROXY_ADMIN cross-user update allowed. |
| tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py | Adds six regression tests for GHSA-q775 covering unlimited delegation, ceiling enforcement, within-ceiling allowed, default-fill bypass, and admin bypass. |
Reviews (9): Last reviewed commit: "chore: trigger re-review" | Re-trigger Greptile
…-q775) Non-admin callers whose API key has no explicit max_budget (None) could bypass the delegated-authority ceiling and create keys with arbitrary budgets. Now blocks budget assignment when caller has no budget configured. Also removes redundant inline import of LitellmUserRoles. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@greptile review |
Capture the caller-supplied max_budget before _enforce_upperbound_key_params can fill it with a default, so auto-filled defaults don't trigger the ceiling guard for non-admin users with no budget on their own key. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@greptile review |
|
|
||
| if ( | ||
| user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value | ||
| and data.max_budget is not None |
There was a problem hiding this comment.
High: Delegated budget can be omitted
The ceiling only runs when the child key supplies max_budget. An authenticated non-admin with a capped key can call /key/generate without max_budget and omit user_id, producing a new key that is not capped by the caller's budget; require generated keys to inherit the caller's limit or reject missing max_budget for capped non-admin callers.
| fallback_spend=0.0, | ||
| ) | ||
| if window_spend >= w["max_budget"]: | ||
| if math.isfinite(w["max_budget"]) and window_spend >= w["max_budget"]: |
There was a problem hiding this comment.
High: Non-finite window budgets are treated as unlimited
budget_limits[].max_budget is accepted from key and team request bodies, but it is not included in the finite-number validation added here. Because this runtime check skips NaN/Inf, an authenticated caller who can create a key budget window can submit a non-finite window budget and that window will never block spend; reject non-finite nested budget values at write time and treat non-finite values as invalid during enforcement.
Budget validation and authorization checks tightenedThis PR adds finite-number validation to several budget management paths and adds non-admin self-update guards around user budget fields. I reviewed the changed runtime budget checks, key generation ceiling logic, and user update authorization flow for new attacker-reachable bypasses in the diff. Status: 5 open |
|
@greptile review |
Move _requested_max_budget capture before both default_key_generate_params and upperbound_key_generate_params mutations, so auto-filled values don't trigger the ceiling check for non-admin users. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@greptile review |
Callers with max_budget=None (unlimited) can legitimately create budget-capped keys. Only block when caller has an explicit budget and the requested budget exceeds it. Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@greptile review |
| ) | ||
| }, | ||
| ) | ||
| if _requested_max_budget > user_api_key_dict.max_budget: |
There was a problem hiding this comment.
High: Budget multiplication through generated keys
This compares each requested child key budget to the caller's key budget independently. A non-admin caller with a $100 key can call /key/generate repeatedly with max_budget=100 and then spend $100 on each generated key, because virtual-key spend is enforced per token unless there is a separate user/team budget backing it. Require generated keys to share an aggregate budget with the caller, cap by remaining delegated budget, or reject non-admin budget delegation unless a user/team-level budget enforces the total.
|
@greptile review |
| fallback_spend=user_object.spend or 0.0, | ||
| ) | ||
| if user_spend >= user_budget: | ||
| if math.isfinite(user_budget) and user_spend >= user_budget: |
There was a problem hiding this comment.
Medium: Non-finite user budget bypass
The runtime budget check skips enforcement for non-finite user_budget values. If /user/new accepts max_budget without finite-number validation, a caller allowed to create users can send NaN or Infinity and receive a user key whose user-level spend cap is skipped here. Fail closed at enforcement or add equivalent validation on user creation.
| if math.isfinite(user_budget) and user_spend >= user_budget: | |
| if not math.isfinite(user_budget) or user_spend >= user_budget: |
- Extract inline audit log block from _update_single_user_helper into
_schedule_user_update_audit_log to bring statement count below 50
- Update test_validate_max_budget assertion to match current error message
("must be a non-negative finite number" not "cannot be negative")
Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
| # Non-admins cannot modify budget-sensitive fields even on their own record | ||
| # (GHSA-wvg4-6222-3q4r). | ||
| if user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value: | ||
| _protected_fields = ("max_budget", "soft_budget", "spend") |
There was a problem hiding this comment.
High: User budget reset bypass
budget_duration is accepted on self-updates, and _update_internal_user_params() converts it into a new budget_reset_at. A non-admin user with a capped budget can call /user/update on their own record with budget_duration: "1s", causing their spend to reset every second instead of using the admin-configured budget window.
| _protected_fields = ("max_budget", "soft_budget", "spend") | |
| _protected_fields = ("max_budget", "soft_budget", "spend", "budget_duration") |
|
@greptile review |
The merge of litellm_internal_staging into this branch dropped the closing `)` from verbose_proxy_logger.warning(...) in _schedule_user_update_audit_log, causing a SyntaxError at import time. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
…on tests The budget protection in /user/update was blocking ALL non-PROXY_ADMIN users from modifying budget fields, even when updating other users' records (which can_user_call_user_update already restricts). Scoping the guard to self-updates makes the intent clearer and avoids false-positive backwards-incompatibility concerns for admin workflows. Adds three regression tests for GHSA-wvg4-6222-3q4r: - Non-admin self-escalation of max_budget → blocked - Non-admin self-escalation of spend → blocked - Proxy admin updating another user's budget → allowed Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
@greptile review |
Co-Authored-By: Claude Opus 4.6 <[email protected]>
Satisfies MyPy union-attr check: prisma_client can be None at import time so guard early before accessing .db. Co-Authored-By: Claude Sonnet 4.6 (1M context) <[email protected]>
410ce76
into
litellm_internal_staging
…I#27897) * fix: block NaN/Inf budget bypass and add missing non-admin guards Addresses three security issues: GHSA-wvg4-6222-3q4r: /user/update exposes max_budget, soft_budget, spend to self-editing non-admin users with no server-side guard. Non-admin callers now receive HTTP 403 if any of those fields appear in the update payload. GHSA-q775-qw9r-2r4g: _enforce_upperbound_key_params returned early (no-op) when upperbound_key_generate_params was absent from config, letting any authenticated user generate a key with unlimited max_budget. Fix adds a delegated-authority ceiling in _common_key_generation_helper: non-admins cannot grant a key more budget than their own key carries. GHSA-2rv4-xv66-fpjg: float('nan') passes every `value < 0` guard because nan < 0 is False in Python, and spend >= nan is always False, permanently disabling budget enforcement for any entity carrying a NaN max_budget. All write-time budget guards now use `not math.isfinite(v) or v < 0`. _enforce_upperbound_key_params validates finiteness unconditionally (before the early-return). All spend-enforcement comparisons in auth_checks.py are now guarded with math.isfinite(max_budget) as defense-in-depth. * fix: close budget ceiling bypass for callers with no max_budget (GHSA-q775) Non-admin callers whose API key has no explicit max_budget (None) could bypass the delegated-authority ceiling and create keys with arbitrary budgets. Now blocks budget assignment when caller has no budget configured. Also removes redundant inline import of LitellmUserRoles. * fix: only apply budget ceiling to explicitly requested max_budget Capture the caller-supplied max_budget before _enforce_upperbound_key_params can fill it with a default, so auto-filled defaults don't trigger the ceiling guard for non-admin users with no budget on their own key. * fix: capture requested max_budget before any defaults are applied Move _requested_max_budget capture before both default_key_generate_params and upperbound_key_generate_params mutations, so auto-filled values don't trigger the ceiling check for non-admin users. * fix: allow unlimited-budget callers to delegate any budget Callers with max_budget=None (unlimited) can legitimately create budget-capped keys. Only block when caller has an explicit budget and the requested budget exceeds it. --------- Co-authored-by: yuneng-jiang <[email protected]> Co-authored-by: ryan-crabbe-berri <[email protected]>
Summary
Test plan
make test-unitpasses