fix(proxy): validate key duration at the write boundary

This commit is contained in:
soroush5 2026-09-04 09:21:16 +03:30 • committed by soroush5
parent 9a715df212
commit 45ff06c713
3 changed files with 45 additions and 0 deletions

View file

@ -23,6 +23,27 @@ def validate_finite_spend(spend: float | None) -> None:
)
def validate_key_duration(duration: str | None) -> None:
"""Reject key-expiry durations that can't be parsed, so a bad value 400s
at the write boundary instead of 500ing inside duration math.
`None` (never expires) and `'-1'` (the update path's never-expire
sentinel) pass through.
"""
if duration is None or duration == "-1":
return
from litellm.litellm_core_utils.duration_parser import duration_in_seconds
try:
duration_in_seconds(duration)
except (ValueError, OverflowError):
raise HTTPException(
status_code=400,
detail={"error": f"Invalid duration '{duration}'. Use a format like '1h', '24h', '7d', or '30d'."},
)
def validate_budget_duration(budget_duration: str | None, status_code: int = 400) -> None:
"""Reject budget durations that can't be parsed, are non-positive, or
overflow date math, so a bad value can't be persisted and later crash the

View file

@ -92,6 +92,7 @@ from litellm.proxy.management_endpoints.common_utils import (
_user_has_admin_view,
validate_budget_duration,
validate_finite_spend,
validate_key_duration,
)
from litellm.proxy.management_endpoints.model_management_endpoints import (
_add_model_to_db,
@ -1002,6 +1003,7 @@ async def _common_key_generation_helper(
)
validate_budget_duration(data.budget_duration)
validate_key_duration(data.duration)
raise_on_invalid_key_logging_config(data.metadata)
if data.throttle_on_budget_exceeded is True and user_api_key_dict.user_role != LitellmUserRoles.PROXY_ADMIN.value:
@ -2695,6 +2697,7 @@ async def _validate_update_key_data(
# Reject NaN/±inf spend before it can reach the DB / spend counter.
validate_finite_spend(data.spend)
validate_budget_duration(data.budget_duration)
validate_key_duration(data.duration)
_is_proxy_admin: Final = user_api_key_dict.user_role == LitellmUserRoles.PROXY_ADMIN.value

View file

@ -29,6 +29,7 @@ from litellm.proxy.management_endpoints.common_utils import (
_user_has_admin_privileges,
_user_has_admin_view,
admin_can_invite_user,
validate_key_duration,
)
from litellm.proxy.management_endpoints.common_utils import _has_non_empty_value
@ -1120,3 +1121,23 @@ class TestUpdateMetadataFieldsPremiumCheck:
}
_update_metadata_fields(updated_kv)
mock_check.assert_called()
class TestValidateKeyDuration:
"""Invalid key-expiry durations must 400, not 500 (#39710)."""
def test_none_and_never_expire_sentinel_pass(self):
validate_key_duration(None)
validate_key_duration("-1")
def test_valid_durations_pass(self):
validate_key_duration("7d")
validate_key_duration("1h")
def test_garbage_durations_raise_400(self):
from fastapi import HTTPException
for bad in ["banana", "1x", ""]:
with pytest.raises(HTTPException) as exc_info:
validate_key_duration(bad)
assert exc_info.value.status_code == 400