diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index a3679b84bd6..df311bed7b2 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -42,7 +42,7 @@ from litellm.proxy._experimental.mcp_server.db import ( rotate_mcp_user_env_vars_master_key, ) from litellm.proxy._types import * -from litellm.proxy._types import LiteLLM_VerificationToken +from litellm.proxy._types import LiteLLM_VerificationToken, hash_token from litellm.proxy.auth.auth_checks import ( _delete_cache_key_object, can_team_access_model, @@ -3777,7 +3777,10 @@ async def generate_key_helper_fn( return user_data ## CREATE KEY - verbose_proxy_logger.debug("prisma_client: Creating Key= %s", key_data) + verbose_proxy_logger.debug( + "prisma_client: Creating Key= %s", + {**key_data, "token": hash_token(token=token)}, + ) create_key_response = await prisma_client.insert_data(data=key_data, table_name="key") key_data["token_id"] = getattr(create_key_response, "token", None) diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index d7649b524aa..62fc28256cd 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -3592,7 +3592,10 @@ class PrismaClient: """ start_time = time.time() try: - verbose_proxy_logger.debug("PrismaClient: insert_data: %s", data) + verbose_proxy_logger.debug( + "PrismaClient: insert_data: %s", + {**data, "token": self.hash_token(token=data["token"])} if data.get("token") is not None else data, + ) if table_name == "key": token = data["token"] hashed_token = self.hash_token(token=token) diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index c9abdf09a5d..aa24b0199ab 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -529,6 +529,56 @@ async def test_key_generation_with_object_permission(monkeypatch): assert key_insert_calls[0]["data"].get("object_permission_id") == "objperm123" +@pytest.mark.asyncio +async def test_generate_key_debug_log_never_contains_raw_token(monkeypatch, caplog): + """Regression for LIT-4356: /key/generate must never emit the raw virtual key + to a logger, even for short keys that bypass the regex-based + SecretRedactionFilter.""" + import hashlib + import logging + + mock_prisma_client = AsyncMock() + mock_prisma_client.jsonify_object = lambda data: data + mock_prisma_client.db = MagicMock() + + async def _insert_data_side_effect(*args, **kwargs): + if kwargs.get("table_name") == "user": + return MagicMock(models=[], spend=0) + return MagicMock( + token="hashed_token_456", + litellm_budget_table=None, + object_permission=None, + ) + + mock_prisma_client.insert_data = AsyncMock(side_effect=_insert_data_side_effect) + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + monkeypatch.setattr( + "litellm.proxy.management_endpoints.key_management_endpoints.get_ui_settings_cached", + AsyncMock(return_value={}), + ) + + from litellm.proxy._types import GenerateKeyRequest, LitellmUserRoles + from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth + from litellm.proxy.management_endpoints.key_management_endpoints import ( + generate_key_fn, + ) + + raw_key = "sk-short-secret" + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + await generate_key_fn( + data=GenerateKeyRequest(key=raw_key), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="user-1", + ), + ) + + log_text = "\n".join(record.getMessage() for record in caplog.records) + assert raw_key not in log_text + assert hashlib.sha256(raw_key.encode()).hexdigest() in log_text + + @pytest.mark.asyncio @pytest.mark.parametrize( "field,request_kwargs,expected_in_error", diff --git a/tests/test_litellm/proxy/utils/prisma_and_spend/test_prisma_client_writes.py b/tests/test_litellm/proxy/utils/prisma_and_spend/test_prisma_client_writes.py index 4e547b81acc..dd241397e87 100644 --- a/tests/test_litellm/proxy/utils/prisma_and_spend/test_prisma_client_writes.py +++ b/tests/test_litellm/proxy/utils/prisma_and_spend/test_prisma_client_writes.py @@ -10,6 +10,7 @@ from __future__ import annotations import hashlib import json +import logging from types import SimpleNamespace from typing import Any from unittest.mock import AsyncMock, MagicMock @@ -103,6 +104,35 @@ async def test_insert_data_user_organization_fk_raises_400( assert raised.status_code == 400 +@pytest.mark.asyncio +async def test_insert_data_debug_log_hashes_token( + prisma_client: PrismaClient, caplog: pytest.LogCaptureFixture +) -> None: + """Regression for LIT-4356: the raw virtual key must never reach a logger, + even for short/nonstandard key formats that bypass the regex-based + SecretRedactionFilter.""" + token = "sk-short-secret" + expected_hash = hashlib.sha256(token.encode()).hexdigest() + prisma_client.db.litellm_verificationtoken.upsert = AsyncMock(return_value=SimpleNamespace(token=expected_hash)) + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + await prisma_client.insert_data(data={"token": token, "key_alias": "redaction-repro"}, table_name="key") + log_text = "\n".join(record.getMessage() for record in caplog.records) + assert token not in log_text + assert expected_hash in log_text + + +@pytest.mark.asyncio +async def test_insert_data_debug_log_tolerates_none_token( + prisma_client: PrismaClient, caplog: pytest.LogCaptureFixture +) -> None: + """A None token must not crash the redacting debug log added for LIT-4356.""" + prisma_client.db.litellm_usertable.upsert = AsyncMock(return_value=SimpleNamespace(user_id="u1")) + with caplog.at_level(logging.DEBUG, logger="LiteLLM Proxy"): + result = await prisma_client.insert_data(data={"user_id": "u1", "token": None}, table_name="user") + assert result.user_id == "u1" + assert any("insert_data" in record.getMessage() for record in caplog.records) + + @pytest.mark.asyncio async def test_insert_data_logs_and_raises_generic_error( prisma_client: PrismaClient,