diff --git a/litellm/proxy/hooks/global_tag_rate_limits_hook.py b/litellm/proxy/hooks/global_tag_rate_limits_hook.py index 09c2230338c..f5d6857a18f 100644 --- a/litellm/proxy/hooks/global_tag_rate_limits_hook.py +++ b/litellm/proxy/hooks/global_tag_rate_limits_hook.py @@ -47,7 +47,6 @@ from litellm._logging import verbose_proxy_logger from litellm.caching.dual_cache import DualCache from litellm.caching.in_memory_cache import InMemoryCache from litellm.integrations.custom_logger import CustomLogger -from litellm.litellm_core_utils.core_helpers import get_metadata_variable_name_from_kwargs from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.common_utils.proxy_rate_limit_error import ProxyRateLimitError from litellm.proxy.hooks.parallel_request_limiter_v3 import ( @@ -112,7 +111,7 @@ from litellm.proxy.hooks.tag_rate_limits_shared import ( policy_fingerprint as _policy_fingerprint, ) from litellm.proxy.hooks.tag_rate_limits_shared import ( - resolve_success_event_metadata_variable_name as _resolve_success_event_metadata_variable_name, + resolve_authoritative_metadata_variable_name as _resolve_authoritative_metadata_variable_name, ) from litellm.proxy.utils import InternalUsageCache from litellm.router_strategy.tag_based_routing import ( @@ -544,7 +543,12 @@ class _PROXY_GlobalTagRateLimitsHook( # pyright: ignore[reportUnusedClass] # o # re-charging. stash: Final = _claim_stash_for_data(data) - metadata_variable_name: Final = get_metadata_variable_name_from_kwargs(data) + # Not get_metadata_variable_name_from_kwargs (naive key-presence + # check): a caller can forge an empty (or None) litellm_metadata on + # an ordinary request to make that check pick it over the real, + # populated metadata the proxy wrote identity/tags into, seeing no + # tags at all and admitting past every configured limit. + metadata_variable_name: Final = _resolve_authoritative_metadata_variable_name(data) tags: Final = _order_tags_for_identity_resolution( _get_tags_from_request_kwargs(data, metadata_variable_name=metadata_variable_name), data, @@ -716,7 +720,7 @@ class _PROXY_GlobalTagRateLimitsHook( # pyright: ignore[reportUnusedClass] # o litellm_params_for_metadata: Final[Mapping[str, object]] = ( litellm_params_raw if isinstance(litellm_params_raw, Mapping) else kwargs ) - metadata_variable_name: Final = _resolve_success_event_metadata_variable_name(litellm_params_for_metadata) + metadata_variable_name: Final = _resolve_authoritative_metadata_variable_name(litellm_params_for_metadata) key_hash: Final = _extract_key_hash(litellm_params_for_metadata, metadata_variable_name) key_alias: Final = _extract_key_alias(litellm_params_for_metadata, metadata_variable_name) diff --git a/tests/test_litellm/proxy/hooks/test_global_tag_rate_limits_hook.py b/tests/test_litellm/proxy/hooks/test_global_tag_rate_limits_hook.py index 6f0d7d1aaf2..1a43bebb3b8 100644 --- a/tests/test_litellm/proxy/hooks/test_global_tag_rate_limits_hook.py +++ b/tests/test_litellm/proxy/hooks/test_global_tag_rate_limits_hook.py @@ -1509,3 +1509,42 @@ async def test_concurrency_limit_admission_refreshes_ttl_end_to_end(time_control inflight_keys = [key for key in in_memory_cache.ttl_dict if key.endswith(":inflight")] assert len(inflight_keys) == 1 assert in_memory_cache.ttl_dict[inflight_keys[0]] > time.time() + + +# --------------------------------------------------------------------------- +# Admission must resolve the authoritative metadata bucket, not the naive +# key-presence check -- veria-ai finding on the sibling model-based hook, +# same vulnerability class in this hook's own admission call site +# --------------------------------------------------------------------------- + + +@pytest.mark.asyncio +async def test_admission_ignores_a_forged_empty_litellm_metadata_key(time_controller, monkeypatch): + """ + get_metadata_variable_name_from_kwargs picks "litellm_metadata" whenever + that key is merely present, regardless of its value. A caller adding an + empty "litellm_metadata" alongside the real, populated "metadata" made + admission read no tags at all, sailing past every configured limit. + """ + monkeypatch.setattr( + litellm, + "global_tag_rate_limits", + { + "request_limits": { + "limits": [{"name": "daily", "tag_id": "end_user_id", "limit": 1, "period_seconds": 86400}] + } + }, + ) + hook = _make_hook(time_controller) + poisoned = {"metadata": {"tags": ["end_user_id:u1"]}, "litellm_metadata": {}} + + await hook.async_pre_call_hook( + user_api_key_dict=_key(), cache=DualCache(), data={**poisoned, "litellm_call_id": "call-1"}, call_type="completion" + ) + with pytest.raises(ProxyRateLimitError): + await hook.async_pre_call_hook( + user_api_key_dict=_key(), + cache=DualCache(), + data={**poisoned, "litellm_call_id": "call-2"}, + call_type="completion", + )