fix(key_generate): harden GHSA-q775 session-token exemption against default_key_generate_params

Capture _requested_team_id before the default_key_generate_params loop runs and
key the UI/CLI session-token budget-ceiling exemption off it, instead of the
post-defaults data.team_id. On an install that sets
default_key_generate_params.team_id, a session token requesting a personal key
(no explicit team_id) would otherwise have data.team_id auto-filled, flipping
is_ui_session_team_key on and bypassing the delegated-authority ceiling -- the
exact escalation GHSA-q775 closed. Mirrors the existing pre-defaults capture of
_requested_max_budget. Adds a regression test.

https://claude.ai/code/session_01RT583b1khYC3wjLrQ5hT5h
This commit is contained in:
mateo-berri
2026-06-04 01:16:13 +00:00
parent 951012ccfe
commit efeb101ec6
2 changed files with 50 additions and 1 deletions
@@ -687,6 +687,11 @@ async def _common_key_generation_helper( # noqa: PLR0915
# params can fill it, so the ceiling check only fires when the caller
# explicitly requested a budget.
_requested_max_budget = data.max_budget
# Same rationale for team_id: capture it before the defaults loop can inject
# one from default_key_generate_params, so the session-token exemption below
# only fires when the caller actually requested a team key (not a personal
# key whose team_id was auto-filled by config defaults).
_requested_team_id = data.team_id
# check if user set default key/generate params on config.yaml
if litellm.default_key_generate_params is not None:
@@ -720,7 +725,7 @@ async def _common_key_generation_helper( # noqa: PLR0915
# at request time. Personal keys keep the ceiling; nothing else bounds them.
is_ui_session_team_key = (
user_api_key_dict.team_id == UI_SESSION_TOKEN_TEAM_ID
and data.team_id is not None
and _requested_team_id is not None
)
if (
user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value
@@ -11561,3 +11561,47 @@ async def test_ghsa_q775_ui_session_token_personal_key_still_capped():
msg = str(getattr(err, "detail", "")) + str(getattr(err, "message", ""))
assert str(code) == "400"
assert "cannot exceed" in msg.lower()
@pytest.mark.asyncio
async def test_ghsa_q775_ui_session_token_default_team_id_personal_key_still_capped():
"""
Security regression for GHSA-q775: the session-token exemption must key off the
CALLER-supplied team_id, not one injected by default_key_generate_params. On an
install that configures default_key_generate_params.team_id, a UI/CLI session
token (team_id=litellm-dashboard) requesting a personal key (no explicit team_id)
has data.team_id auto-filled by the defaults loop. The ceiling must STILL fire:
if the exemption read the post-defaults data.team_id it would flip on and let a
leaked session token (blast radius $0.25) mint an arbitrary-budget key.
"""
from litellm.constants import UI_SESSION_TOKEN_TEAM_ID
data = GenerateKeyRequest(max_budget=500)
assert data.team_id is None # caller did not request a team key
user_api_key_dict = UserAPIKeyAuth(
user_role=LitellmUserRoles.INTERNAL_USER,
api_key="sk-ui-session",
user_id="user-1",
team_id=UI_SESSION_TOKEN_TEAM_ID,
max_budget=0.25,
)
mock_prisma_client = AsyncMock()
with (
patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client),
patch("litellm.proxy.proxy_server.user_api_key_cache", MagicMock()),
patch("litellm.proxy.proxy_server.user_custom_key_generate", None),
patch("litellm.default_key_generate_params", {"team_id": "team-default"}),
):
with pytest.raises((HTTPException, ProxyException)) as exc_info:
await generate_key_fn(
data=data,
user_api_key_dict=user_api_key_dict,
litellm_changed_by=None,
)
err = exc_info.value
code = getattr(err, "status_code", None) or getattr(err, "code", None)
msg = str(getattr(err, "detail", "")) + str(getattr(err, "message", ""))
assert str(code) == "400"
assert "cannot exceed" in msg.lower()