From a8a566b0240e647d35563447851b45165aa36eb3 Mon Sep 17 00:00:00 2001 From: chyroc <15604894+chyroc@users.noreply.github.com> Date: Tue, 11 Aug 2026 13:15:31 +0000 Subject: [PATCH 1/2] fix(proxy): allow public pass-through routes for virtual keys --- litellm/proxy/auth/route_checks.py | 17 +++++- .../proxy/auth/test_route_checks.py | 56 +++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 04eb7ab326b..bc00f8110f2 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -248,7 +248,9 @@ class RouteChecks: method=RouteChecks._get_request_method(request=request), ): RouteChecks._require_auth_pass_through_access(route=route, valid_token=valid_token) - elif RouteChecks.is_llm_api_route(route=route): + elif RouteChecks.is_llm_api_route(route=route) or RouteChecks.is_public_pass_through_route( + route=route, method=RouteChecks._get_request_method(request=request) + ): pass elif RouteChecks.is_info_route(route=route): # check if user allowed to call an info route @@ -629,6 +631,19 @@ class RouteChecks: return False return route_info.get("auth") is True + @staticmethod + def is_public_pass_through_route(route: str, method: str | None = None) -> bool: + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( + InitPassThroughEndpointHelpers, + ) + + route_info: Final = InitPassThroughEndpointHelpers.get_registered_pass_through_route( + route=route, method=method + ) + if route_info is None: + return False + return route_info.get("auth") is False + @staticmethod def _auth_pass_through_denied_exception(route: str) -> HTTPException: return HTTPException( diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index 0bfb10320f7..fcad511c678 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1176,6 +1176,62 @@ def test_non_proxy_admin_allows_auth_pass_through_with_team_allowlist(): ) +@pytest.mark.parametrize( + "route,registered_type", + [ + ("/myprefix", "exact"), + ("/myprefix/models", "subpath"), + ], +) +def test_non_proxy_admin_allows_non_auth_pass_through_with_llm_api_routes( + route, registered_type +): + mock_registered_routes = { + f"test-uuid-1:{registered_type}:/myprefix:GET": { + "endpoint_id": "test-uuid-1", + "path": "/myprefix", + "type": registered_type, + "methods": ["GET"], + "auth": False, + }, + } + valid_token = UserAPIKeyAuth( + user_id="test_user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + allowed_routes=["llm_api_routes"], + ) + request = MagicMock(spec=Request) + request.method = "GET" + request.query_params = {} + + with ( + patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints._registered_pass_through_routes", + mock_registered_routes, + ), + patch( + "litellm.proxy.utils.get_server_root_path", + return_value="/", + ), + ): + assert ( + RouteChecks.is_virtual_key_allowed_to_call_route( + route=route, + valid_token=valid_token, + request=request, + ) + is True + ) + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=None, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route=route, + request=request, + valid_token=valid_token, + request_data={}, + ) + + def test_virtual_key_without_llm_api_routes_cannot_access_pass_through(): """ Test that virtual keys without llm_api_routes permission cannot access registered pass-through endpoints. From 8fd6f39fa3e99e3305b6ff4209cb797078dc5440 Mon Sep 17 00:00:00 2001 From: chyroc <15604894+chyroc@users.noreply.github.com> Date: Tue, 11 Aug 2026 15:18:57 +0000 Subject: [PATCH 2/2] fix(proxy): guard pass-through route collisions --- litellm/proxy/auth/route_checks.py | 26 ++++-- .../proxy/auth/test_route_checks.py | 85 ++++++++++++++++++- 2 files changed, 103 insertions(+), 8 deletions(-) diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index bc00f8110f2..554a4c69225 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -12,6 +12,9 @@ from litellm.proxy._types import ( LitellmUserRoles, UserAPIKeyAuth, ) +from litellm.types.passthrough_endpoints.pass_through_endpoints import ( + LITELLM_PASS_THROUGH_ENDPOINT_MARKER, +) from .auth_checks_organization import _user_is_org_admin @@ -249,7 +252,9 @@ class RouteChecks: ): RouteChecks._require_auth_pass_through_access(route=route, valid_token=valid_token) elif RouteChecks.is_llm_api_route(route=route) or RouteChecks.is_public_pass_through_route( - route=route, method=RouteChecks._get_request_method(request=request) + route=route, + method=RouteChecks._get_request_method(request=request), + request=request, ): pass elif RouteChecks.is_info_route(route=route): @@ -632,14 +637,25 @@ class RouteChecks: return route_info.get("auth") is True @staticmethod - def is_public_pass_through_route(route: str, method: str | None = None) -> bool: + def is_public_pass_through_route( + route: str, + method: str | None = None, + request: Request | None = None, + ) -> bool: + if request is None: + return False + scope: Final = getattr(request, "scope", None) + if not isinstance(scope, dict): + return False + endpoint: Final = scope.get("endpoint") + if getattr(endpoint, LITELLM_PASS_THROUGH_ENDPOINT_MARKER, False) is not True: + return False + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import ( InitPassThroughEndpointHelpers, ) - route_info: Final = InitPassThroughEndpointHelpers.get_registered_pass_through_route( - route=route, method=method - ) + route_info: Final = InitPassThroughEndpointHelpers.get_registered_pass_through_route(route=route, method=method) if route_info is None: return False return route_info.get("auth") is False diff --git a/tests/test_litellm/proxy/auth/test_route_checks.py b/tests/test_litellm/proxy/auth/test_route_checks.py index fcad511c678..2b2da2c6017 100644 --- a/tests/test_litellm/proxy/auth/test_route_checks.py +++ b/tests/test_litellm/proxy/auth/test_route_checks.py @@ -1200,9 +1200,22 @@ def test_non_proxy_admin_allows_non_auth_pass_through_with_llm_api_routes( user_role=LitellmUserRoles.INTERNAL_USER.value, allowed_routes=["llm_api_routes"], ) - request = MagicMock(spec=Request) - request.method = "GET" - request.query_params = {} + from litellm.types.passthrough_endpoints.pass_through_endpoints import ( + LITELLM_PASS_THROUGH_ENDPOINT_MARKER, + ) + + def pass_through_endpoint(): ... + + setattr(pass_through_endpoint, LITELLM_PASS_THROUGH_ENDPOINT_MARKER, True) + request = Request( + scope={ + "type": "http", + "headers": [], + "method": "GET", + "query_string": b"", + "endpoint": pass_through_endpoint, + } + ) with ( patch( @@ -1232,6 +1245,72 @@ def test_non_proxy_admin_allows_non_auth_pass_through_with_llm_api_routes( ) +@pytest.mark.parametrize( + "registered_path,registered_type", + [ + ("/config/update", "exact"), + ("/config", "subpath"), + ], +) +def test_public_pass_through_registration_does_not_bypass_builtin_route_auth( + registered_path, registered_type +): + mock_registered_routes = { + f"test-uuid-1:{registered_type}:{registered_path}:GET": { + "endpoint_id": "test-uuid-1", + "path": registered_path, + "type": registered_type, + "methods": ["GET"], + "auth": False, + }, + } + valid_token = UserAPIKeyAuth( + user_id="test_user", + user_role=LitellmUserRoles.INTERNAL_USER.value, + allowed_routes=["llm_api_routes"], + ) + + def builtin_config_update(): ... + + request = Request( + scope={ + "type": "http", + "headers": [], + "method": "GET", + "query_string": b"", + "endpoint": builtin_config_update, + } + ) + + with ( + patch( + "litellm.proxy.pass_through_endpoints.pass_through_endpoints._registered_pass_through_routes", + mock_registered_routes, + ), + patch( + "litellm.proxy.utils.get_server_root_path", + return_value="/", + ), + ): + assert ( + RouteChecks.is_virtual_key_allowed_to_call_route( + route="/config/update", + valid_token=valid_token, + request=request, + ) + is True + ) + with pytest.raises(Exception, match="Only proxy admin"): + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=None, + _user_role=LitellmUserRoles.INTERNAL_USER.value, + route="/config/update", + request=request, + valid_token=valid_token, + request_data={}, + ) + + def test_virtual_key_without_llm_api_routes_cannot_access_pass_through(): """ Test that virtual keys without llm_api_routes permission cannot access registered pass-through endpoints.