From 2addb261a52b2480877fba202a8dd0e0dc9ec92c Mon Sep 17 00:00:00 2001 From: mubashir1osmani Date: Thu, 16 Jul 2026 17:27:08 -0700 Subject: [PATCH] fix(team): bound bulk member update payload sizes Reject oversized raw user_ids and allowed_models lists at schema validation, and cap individual user_id, model, and budget_duration string lengths so bulk writes cannot amplify unbounded attacker-controlled input across the batch --- litellm/proxy/_types.py | 38 ++++++++++++++-- .../management_endpoints/team_endpoints.py | 6 +-- .../proxy/test_team_member_update.py | 43 +++++++++++++++++++ 3 files changed, 81 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index a95076d87c3..366ecb4c69d 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3723,13 +3723,33 @@ class TeamMemberUpdateResponse(MemberUpdateResponse): allowed_models: Optional[List[str]] = None +BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS = 500 +BULK_TEAM_MEMBER_UPDATE_MAX_ALLOWED_MODELS = 500 +BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH = 256 +BULK_TEAM_MEMBER_UPDATE_MAX_BUDGET_DURATION_LENGTH = 32 + + class TeamMemberBulkUpdateFields(LiteLLMPydanticObjectBase): max_budget_in_team: float | None = None role: Literal["admin", "user"] | None = None tpm_limit: int | None = None rpm_limit: int | None = None - budget_duration: str | None = None - allowed_models: list[str] | None = None + budget_duration: str | None = Field(default=None, max_length=BULK_TEAM_MEMBER_UPDATE_MAX_BUDGET_DURATION_LENGTH) + allowed_models: list[str] | None = Field(default=None, max_length=BULK_TEAM_MEMBER_UPDATE_MAX_ALLOWED_MODELS) + + @field_validator("allowed_models") + @classmethod + def bound_allowed_model_strings(cls, models: list[str] | None) -> list[str] | None: + if models is None: + return None + for model in models: + if len(model) > BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH: + raise ValueError( + "allowed_models entries must be at most {} characters".format( + BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH + ) + ) + return models @model_validator(mode="after") def require_at_least_one_field(self): @@ -3739,10 +3759,22 @@ class TeamMemberBulkUpdateFields(LiteLLMPydanticObjectBase): class BulkTeamMemberUpdateRequest(LiteLLMPydanticObjectBase): - user_ids: list[str] | None = None + user_ids: list[str] | None = Field(default=None, max_length=BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS) all_members_in_team: bool = False update_fields: TeamMemberBulkUpdateFields + @field_validator("user_ids") + @classmethod + def bound_user_id_strings(cls, user_ids: list[str] | None) -> list[str] | None: + if user_ids is None: + return None + for user_id in user_ids: + if len(user_id) > BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH: + raise ValueError( + "user_ids entries must be at most {} characters".format(BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH) + ) + return user_ids + @model_validator(mode="after") def validate_selection(self): has_user_ids = self.user_ids is not None and len(self.user_ids) > 0 diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 2d1bde52ba1..7c6a8f67224 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -27,6 +27,7 @@ from litellm.integrations.prometheus import PrometheusLogger from litellm.litellm_core_utils.safe_json_dumps import safe_dumps from litellm.proxy._types import ( UI_TEAM_ID, + BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS, BlockTeamRequest, BulkTeamMemberUpdateRequest, BulkTeamMemberUpdateResponse, @@ -3148,13 +3149,12 @@ async def bulk_update_team_members( else: user_ids = list(dict.fromkeys(data.user_ids or [])) - max_batch_size = 500 - if len(user_ids) > max_batch_size: + if len(user_ids) > BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS: raise HTTPException( status_code=400, detail={ "error": "Maximum {} team members can be updated at once. Found {} user_ids.".format( - max_batch_size, len(user_ids) + BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS, len(user_ids) ) }, ) diff --git a/tests/test_litellm/proxy/test_team_member_update.py b/tests/test_litellm/proxy/test_team_member_update.py index 4097ed8ed76..fb9fe091076 100644 --- a/tests/test_litellm/proxy/test_team_member_update.py +++ b/tests/test_litellm/proxy/test_team_member_update.py @@ -549,3 +549,46 @@ def test_bulk_team_member_update_requires_exactly_one_member_selector(): all_members_in_team=True, update_fields=TeamMemberBulkUpdateFields(tpm_limit=42), ) + + +def test_bulk_team_member_update_rejects_raw_user_ids_over_cap(): + from pydantic import ValidationError + + from litellm.proxy._types import BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS + + with pytest.raises(ValidationError): + BulkTeamMemberUpdateRequest( + user_ids=[f"user-{i}" for i in range(BULK_TEAM_MEMBER_UPDATE_MAX_USER_IDS + 1)], + update_fields=TeamMemberBulkUpdateFields(role="user"), + ) + + +def test_bulk_team_member_update_rejects_oversized_allowed_models_payload(): + from pydantic import ValidationError + + from litellm.proxy._types import ( + BULK_TEAM_MEMBER_UPDATE_MAX_ALLOWED_MODELS, + BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH, + ) + + with pytest.raises(ValidationError): + BulkTeamMemberUpdateRequest( + user_ids=["user-1"], + update_fields=TeamMemberBulkUpdateFields( + allowed_models=[f"model-{i}" for i in range(BULK_TEAM_MEMBER_UPDATE_MAX_ALLOWED_MODELS + 1)] + ), + ) + + with pytest.raises(ValidationError): + BulkTeamMemberUpdateRequest( + user_ids=["user-1"], + update_fields=TeamMemberBulkUpdateFields( + allowed_models=["m" * (BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH + 1)] + ), + ) + + with pytest.raises(ValidationError): + BulkTeamMemberUpdateRequest( + user_ids=["u" * (BULK_TEAM_MEMBER_UPDATE_MAX_STRING_LENGTH + 1)], + update_fields=TeamMemberBulkUpdateFields(role="user"), + )