From effd233d96183ef01180c24740cf197573ca8979 Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Thu, 10 Sep 2026 14:12:53 -0700 Subject: [PATCH] fix(proxy): decode nested percent-encoded file ids iteratively An id percent-encoded a thousand or more times sent the recursive decoder past the interpreter's recursion limit, so a proxy admin key got a 500 instead of a verdict on the id. The decoder now walks the encodings in a loop, so any depth resolves --- litellm/litellm_core_utils/cloud_storage_security.py | 5 +++-- .../litellm_core_utils/test_cloud_storage_security.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/cloud_storage_security.py b/litellm/litellm_core_utils/cloud_storage_security.py index a7aa0d09e9e..2d7eaac676f 100644 --- a/litellm/litellm_core_utils/cloud_storage_security.py +++ b/litellm/litellm_core_utils/cloud_storage_security.py @@ -1,6 +1,7 @@ import posixpath import re from collections.abc import Mapping, Sequence +from itertools import accumulate, pairwise, repeat from types import MappingProxyType from typing import Any, Final, cast from urllib.parse import quote, unquote @@ -21,8 +22,8 @@ _MAPPING_PROXY_TYPE: Final[type] = type(MappingProxyType({})) def _fully_unquoted(value: str) -> str: - decoded: Final = unquote(value) - return value if decoded == value else _fully_unquoted(decoded) + decodings: Final = accumulate(repeat(value), lambda current, _: unquote(current)) + return next(current for current, following in pairwise(decodings) if current == following) def is_managed_cloud_storage_uri(file_id: str) -> bool: diff --git a/tests/test_litellm/litellm_core_utils/test_cloud_storage_security.py b/tests/test_litellm/litellm_core_utils/test_cloud_storage_security.py index 66be143054a..db1dba12965 100644 --- a/tests/test_litellm/litellm_core_utils/test_cloud_storage_security.py +++ b/tests/test_litellm/litellm_core_utils/test_cloud_storage_security.py @@ -28,3 +28,11 @@ def test_is_managed_cloud_storage_uri_ignores_provider_and_unified_ids(): ) def test_is_managed_cloud_storage_uri_sees_through_percent_encoding(file_id: str): assert is_managed_cloud_storage_uri(file_id) + + +def test_is_managed_cloud_storage_uri_survives_an_id_encoded_thousands_of_times(): + nested_gs_id = "gs" + "%" + "25" * 5000 + "3A//bucket/x" + nested_plain_id = "%" + "25" * 5000 + + assert is_managed_cloud_storage_uri(nested_gs_id) + assert not is_managed_cloud_storage_uri(nested_plain_id)