From a40804b7dc281fd367d5d7a8f6681c53b10e1bb6 Mon Sep 17 00:00:00 2001 From: mateo Date: Tue, 15 Sep 2026 18:06:57 +0000 Subject: [PATCH] fix(proxy): block POST /user/new during the master key lockout Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/auth/master_key_policy.py | 8 ++++++-- tests/test_litellm/proxy/auth/test_master_key_policy.py | 3 +++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/master_key_policy.py b/litellm/proxy/auth/master_key_policy.py index 0386461382d..e9fece1c01e 100644 --- a/litellm/proxy/auth/master_key_policy.py +++ b/litellm/proxy/auth/master_key_policy.py @@ -41,6 +41,8 @@ class _DeploymentMarker(TypedDict, total=False): _DEPLOYMENT_MARKERS: Final = TypeAdapter(list[_DeploymentMarker]) +_VIRTUAL_KEY_MINTING_ROUTES: Final = ("/user/new",) + _ACCESS_CREDENTIAL_ROUTES: Final = ( "/credentials", "/credentials/by_name/{credential_name:path}", @@ -121,7 +123,7 @@ def request_http_method(request: Request) -> str: method: Final = request.method except (KeyError, AttributeError): return "" - return method if isinstance(method, str) else "" + return method if isinstance(method, str) else "" # pyright: ignore[reportUnnecessaryIsInstance] # spec'd request mocks return a Mock, not a str def master_key_lockout_action( @@ -140,7 +142,9 @@ def master_key_lockout_action( return "access_credentials" if method.upper() != "GET" and RouteChecks.is_llm_api_route(route=route): return "use_credentials" - if method.upper() != "GET" and _route_matches_any(route, tuple(LiteLLMRoutes.key_management_routes.value)): + if method.upper() != "GET" and _route_matches_any( + route, tuple(LiteLLMRoutes.key_management_routes.value) + _VIRTUAL_KEY_MINTING_ROUTES + ): return "manage_virtual_keys" return None diff --git a/tests/test_litellm/proxy/auth/test_master_key_policy.py b/tests/test_litellm/proxy/auth/test_master_key_policy.py index 863715d991e..112c52fd88d 100644 --- a/tests/test_litellm/proxy/auth/test_master_key_policy.py +++ b/tests/test_litellm/proxy/auth/test_master_key_policy.py @@ -102,7 +102,9 @@ def test_insecure_master_key_warning_survives_redaction(): ("/key/abc-def/regenerate", "POST", "manage_virtual_keys"), ("/key/service-account/generate", "POST", "manage_virtual_keys"), ("/key/block", "POST", "manage_virtual_keys"), + ("/user/new", "POST", "manage_virtual_keys"), ("/key/info", "GET", None), + ("/user/info", "GET", None), ("/key/list", "GET", None), ("/login", "POST", None), ("/health/readiness", "GET", None), @@ -140,6 +142,7 @@ def test_master_key_lockout_action_none_when_key_secure(route, method): ("/chat/completions", "POST"), ("/key/generate", "POST"), ("/key/abc/regenerate", "POST"), + ("/user/new", "POST"), ], ) @pytest.mark.parametrize("reason", ["example_key", "missing"])