Repository navigation
LiteLLM: a body-level fallbacks array bypasses the team model ACL #41
Description
The mechanism, precisely
It is not that the fallbacks array bypasses auth. Auth does inspect it. The problem is that the only control which runs over the array is the one we have deliberately emptied.
- The key-level check DOES validate every entry.
_enforce_key_and_fallback_model_access(user_api_key_auth.py:2147-2154) callscan_key_call_modelfor each element ofrequest_data["fallbacks"]. - But it returns all-access for our keys.
auth_checks.py:2712:if (len(filtered_models) == 0 and len(models) == 0) … all_model_access = True. All 95 enterprise keys carrymodels='{}', delegating to the team list, so every entry passes. - The team-level check never looks at the array.
common_checks/can_team_access_modelvalidate only the primarymodel;grep -n "fallbacks" auth_checks.pyreturns nothing. is_valid_fallback_modelonly verifies the target group is routable.
Net effect: any org key can reach any registered model group by naming it in a body-level fallbacks array, regardless of its team's models list.
This framing matters for where the fix belongs: the router does not need patching. The gap is in the key/team access model.
Verified 2026-09-08 in litellm-test, both directions:
| result | |
|---|---|
| key whose team lists only the primary, asking for the fallback group directly | 403 "This API key does not have access to the requested model" |
| same key, primary fails, router falls back to that group | served |
The asymmetry is what makes model-group fallbacks work at all, so it is not simply a bug to close.
Why it is not currently exploitable
Verified after the 2026-09-08 cleanup, both tenants:
enterprise : orphan groups (registered, in no team): none
community : orphan groups: deepseek-v4-flash-fallback → in FALLBACK_GROUP_CANON
There is no unmetered destination today. Enterprise has zero orphan groups since deepseek-v4-flash-direct-parked was deleted; community has one and it is canonicalised, so reaching it via a body fallbacks array consumes the primary's cap.
Why it still needs an owner
The exposure is a class, not an instance. Any future group that is registered but (a) absent from every team's models, (b) absent from GATED_MODELS / GATED_MODELS_CSV, and (c) absent from FALLBACK_GROUP_CANON is immediately reachable and unmetered. Nothing in CI notices: the invariant added in nan-devops#278 walks the fallbacks: lists, so a bare orphan group is invisible to it.
That is exactly how deepseek-v4-flash-direct-parked appeared — created as a mitigation, not a feature, and it opened a paid unmetered path for ~90 minutes before anyone looked.
Options
Given the mechanism above, the router is not the place to fix this:
- Guard the class in CI. Extend the nan-devops invariant to walk the registered model list for groups with a third-party
api_baseand require each to be in a team, gated, or canonicalised. Catches orphans however they appear. Cheap, high value, should happen regardless. - Give enterprise keys a real
modelslist instead of{}. This is the actual fix: it removes the delegation that makescan_key_call_modelreturn all-access. Broad change to key provisioning, needs a migration for 95 keys, deserves its own assessment. - Reject a body-level
fallbacksat the edge for tenant traffic. Blunt, and would break any legitimate client use. - Detect rather than prevent: alert when a request's resolved group is outside the caller's team list.
Evidence trail
- nan-devops#278, #284, #285 and their review threads.
feedback_litellm_fallback_bypasses_team_aclin pi memory.
Not urgent, deliberately: no live unmetered destination exists today.
Activity