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
This commit is contained in:
mubashir1osmani 2026-07-16 17:27:08 -07:00
parent 1788560aef
commit 2addb261a5
3 changed files with 81 additions and 6 deletions

View file

@ -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

View file

@ -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)
)
},
)

View file

@ -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"),
)