mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
1788560aef
commit
2addb261a5
3 changed files with 81 additions and 6 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue