From 2c9faf2c407669d532f6514db62e59364e9c586a Mon Sep 17 00:00:00 2001 From: Joshua Valluru <326636767+joshua-berri@users.noreply.github.com> Date: Mon, 21 Sep 2026 16:26:52 -0700 Subject: [PATCH] fix(mcp): hash connection credentials in auth failure metadata --- litellm/proxy/_types.py | 2 +- .../proxy/auth/test_auth_exception_handler.py | 11 ++++++----- tests/test_litellm/proxy/test__types.py | 10 ++++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 0592616f06a..6bf846cd2de 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -3300,7 +3300,7 @@ class UserAPIKeyAuth(LiteLLM_VerificationTokenView): # the expected response ob normalized = api_key if normalized[:7].lower() == "bearer ": normalized = normalized[7:] - if normalized.startswith("sk-"): + if normalized.startswith(("sk-", "llm_caccess_", "llm_crefresh_")): return hash_token(normalized) from litellm.proxy.auth.handle_jwt import JWTHandler diff --git a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py index 3edc57af124..352cca70ec8 100644 --- a/tests/test_litellm/proxy/auth/test_auth_exception_handler.py +++ b/tests/test_litellm/proxy/auth/test_auth_exception_handler.py @@ -594,9 +594,10 @@ async def test_resolved_identity_exported_on_auth_failure(): @pytest.mark.asyncio -async def test_auth_failure_without_resolved_identity_still_logs(): +@pytest.mark.parametrize("api_key", ["sk-unknown", "llm_caccess_sealed-test-access", "llm_crefresh_sealed-test-refresh"]) +async def test_auth_failure_without_resolved_identity_still_logs(api_key): """When auth fails before any identity is resolved (e.g. an unknown key), - the handler must still log a usable object carrying the raw api key and + the handler must still log a usable object carrying the hashed api key and route, not crash on the missing identity.""" handler = UserAPIKeyAuthExceptionHandler() @@ -625,14 +626,14 @@ async def test_auth_failure_without_resolved_identity_still_logs(): {}, "/v1/chat/completions", None, - "sk-unknown", + api_key, ) logged = mock_hook.call_args[1]["user_api_key_dict"] # Raw key must NOT land on the object — it would be promoted into telemetry # as litellm.api_key.hash and leak a real sk-... to anyone reading the trace. - assert logged.api_key != "sk-unknown" - assert logged.api_key == UserAPIKeyAuth(api_key="sk-unknown").api_key + assert logged.api_key != api_key + assert logged.api_key == UserAPIKeyAuth(api_key=api_key).api_key assert logged.request_route == "/v1/chat/completions" diff --git a/tests/test_litellm/proxy/test__types.py b/tests/test_litellm/proxy/test__types.py index 9e1486ce90f..8f7859129bb 100644 --- a/tests/test_litellm/proxy/test__types.py +++ b/tests/test_litellm/proxy/test__types.py @@ -48,8 +48,9 @@ def test_the_server_sets_a_marker_by_assignment_after_construction(marker): assert getattr(auth, marker) == "set-by-the-server" -def test_a_virtual_key_is_hashed_out_of_the_auth_object(): - raw_key = "sk-1234567890abcdefghij" +@pytest.mark.parametrize("prefix", ["sk-", "llm_caccess_", "llm_crefresh_"]) +def test_a_virtual_key_is_hashed_out_of_the_auth_object(prefix): + raw_key = prefix + "1234567890abcdefghij" auth = UserAPIKeyAuth(api_key=raw_key) @@ -57,8 +58,9 @@ def test_a_virtual_key_is_hashed_out_of_the_auth_object(): assert auth.token == auth.api_key -def test_a_bearer_prefixed_key_hashes_the_same_as_the_bare_key(): - raw_key = "sk-1234567890abcdefghij" +@pytest.mark.parametrize("prefix", ["sk-", "llm_caccess_", "llm_crefresh_"]) +def test_a_bearer_prefixed_key_hashes_the_same_as_the_bare_key(prefix): + raw_key = prefix + "1234567890abcdefghij" assert UserAPIKeyAuth(api_key=f"Bearer {raw_key}").token == UserAPIKeyAuth(api_key=raw_key).token