fix(batch_rate_limiter): enforce model access check before honoring skip paths

Admin-configured skips (disable_batch_input_file_rate_limiting,
skip_batch_input_file_rate_limiting_for_models/_for_providers) and the
no-applicable-rate-limits short-circuit previously bypassed
_enforce_batch_file_model_access. A key with a restricted model
allowlist could therefore submit a batch JSONL referencing models
outside its allowlist whenever any of these skip paths fired, and the
provider-skip path was attacker-controllable via the request body's
custom_llm_provider field. Hoist the model-access guard to the top so
restricted keys always have their JSONL validated regardless of which
skip would otherwise apply.

Co-authored-by: Yassin Kortam <yassin@berri.ai>
This commit is contained in:
Cursor Agent 2026-05-28 06:35:11 +00:00
parent b391f772ad
commit 4f9ceddd52
No known key found for this signature in database
2 changed files with 25 additions and 25 deletions

View file

@ -145,23 +145,25 @@ class _PROXY_BatchRateLimiter(CustomLogger):
"""
Skip downloading batch input files when configured or when there is
nothing to enforce (no applicable rate limits and no model allowlist).
When the caller's key has a model allowlist to enforce, no skip path
is honored: the JSONL must still be downloaded so
``_enforce_batch_file_model_access`` can validate every ``body.model``
entry. Otherwise a restricted key could smuggle unauthorized models
into the file via an admin-configured skip (global disable,
per-model, per-provider) or via the user-controlled
``custom_llm_provider`` field.
"""
if self._key_requires_batch_model_access_check(user_api_key_dict):
return False
from litellm.proxy.proxy_server import general_settings
if general_settings.get("disable_batch_input_file_rate_limiting") is True:
return True
# Only honor the metadata-based skip when the key has no model
# allowlist to enforce. Otherwise a caller could set this flag in
# the request body (it lands in ``litellm_metadata`` for batch
# routes) and skip ``_enforce_batch_file_model_access``, smuggling
# restricted models into the JSONL.
litellm_metadata = data.get("litellm_metadata") or {}
if litellm_metadata.get(
"skip_batch_input_file_rate_limiting"
) is True and not self._key_requires_batch_model_access_check(
user_api_key_dict
):
if litellm_metadata.get("skip_batch_input_file_rate_limiting") is True:
return True
batch_model = self._get_batch_routing_model(data)
@ -189,19 +191,18 @@ class _PROXY_BatchRateLimiter(CustomLogger):
)
return True
if not self._key_requires_batch_model_access_check(user_api_key_dict):
descriptors = self.parallel_request_limiter._create_rate_limit_descriptors(
user_api_key_dict=user_api_key_dict,
data=data,
rpm_limit_type=None,
tpm_limit_type=None,
model_has_failures=False,
descriptors = self.parallel_request_limiter._create_rate_limit_descriptors(
user_api_key_dict=user_api_key_dict,
data=data,
rpm_limit_type=None,
tpm_limit_type=None,
model_has_failures=False,
)
if not self._has_applicable_batch_rate_limits(descriptors):
verbose_proxy_logger.debug(
"Skipping batch input file processing: no rate limits configured"
)
if not self._has_applicable_batch_rate_limits(descriptors):
verbose_proxy_logger.debug(
"Skipping batch input file processing: no rate limits configured"
)
return True
return True
return False

View file

@ -14,7 +14,6 @@ from fastapi import HTTPException
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
# ---------------------------------------------------------------------------
# Token counter — covers all three batch payload shapes
# ---------------------------------------------------------------------------
@ -268,7 +267,7 @@ async def test_pre_call_skips_file_fetch_when_disabled_in_general_settings():
internal_usage_cache=MagicMock(),
parallel_request_limiter=MagicMock(),
)
user = UserAPIKeyAuth(api_key="sk-ok", user_id="alice", models=["gpt-4o"])
user = UserAPIKeyAuth(api_key="sk-ok", user_id="alice", models=["*"])
with patch(
"litellm.proxy.proxy_server.general_settings",
@ -293,7 +292,7 @@ async def test_pre_call_skips_file_fetch_for_configured_provider():
internal_usage_cache=MagicMock(),
parallel_request_limiter=MagicMock(),
)
user = UserAPIKeyAuth(api_key="sk-ok", user_id="alice", models=["gpt-4o"])
user = UserAPIKeyAuth(api_key="sk-ok", user_id="alice", models=["*"])
with patch(
"litellm.proxy.proxy_server.general_settings",