fix(proxy): stop logging raw API key in AuditLog on key update

The /key/update audit hook stored the raw request key in object_id and
updated_values, unlike the create/rotate/delete hooks which reference the
hashed token. Use existing_key_row.token for object_id and drop the raw key
from updated_values so the AuditLog never persists the plaintext sk- value.

Fixes #31620

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
shivam 2026-07-23 01:11:49 +00:00
parent d25bac5a41
commit 494d2166fa
2 changed files with 56 additions and 3 deletions

View file

@ -88,7 +88,7 @@ class KeyManagementEventHooks:
@staticmethod
async def async_key_updated_hook(
data: UpdateKeyRequest,
existing_key_row: Any,
existing_key_row: LiteLLM_VerificationToken,
response: Any,
user_api_key_dict: UserAPIKeyAuth,
litellm_changed_by: Optional[str] = None,
@ -107,7 +107,7 @@ class KeyManagementEventHooks:
# Enterprise Feature - Audit Logging. Enable with litellm.store_audit_logs = True
if litellm.store_audit_logs is True:
_updated_values = json.dumps(data.json(exclude_none=True), default=str)
_updated_values = json.dumps(data.json(exclude_none=True, exclude={"key"}), default=str)
_before_value = existing_key_row.json(exclude_none=True)
_before_value = json.dumps(_before_value, default=str)
@ -124,7 +124,7 @@ class KeyManagementEventHooks:
),
changed_by_api_key=user_api_key_dict.api_key,
table_name=LitellmTableNames.KEY_TABLE_NAME,
object_id=data.key,
object_id=existing_key_row.token or "",
action="updated",
updated_values=_updated_values,
before_value=_before_value,

View file

@ -155,6 +155,59 @@ class TestKeyManagementEventHooksIndependentOperations:
assert email_called["called"] is True
class TestKeyUpdateAuditLogDoesNotLeakRawKey:
"""Regression tests for issue #31620 - raw API key leaked in AuditLog."""
@pytest.mark.asyncio
async def test_updated_hook_uses_hashed_token_and_hides_raw_key(self):
"""
/key/update must reference the key by its hashed token in the audit log,
never the raw sk- value, in either object_id or updated_values.
"""
import asyncio
from litellm.proxy._types import (
LiteLLM_VerificationToken,
UpdateKeyRequest,
UserAPIKeyAuth,
)
raw_key = "sk-super-secret-raw-key-should-never-be-logged"
hashed_token = "a" * 64
data = UpdateKeyRequest(key=raw_key, max_budget=2000.0)
existing_key_row = LiteLLM_VerificationToken(
token=hashed_token,
key_name="sk-...oged",
max_budget=100.0,
)
captured = {}
async def fake_create_audit_log_for_update(request_data):
captured["log"] = request_data
with (
patch(
"litellm.proxy.management_helpers.audit_logs.create_audit_log_for_update",
side_effect=fake_create_audit_log_for_update,
),
patch("litellm.store_audit_logs", True),
):
await KeyManagementEventHooks.async_key_updated_hook(
data=data,
existing_key_row=existing_key_row,
response=existing_key_row,
user_api_key_dict=UserAPIKeyAuth(api_key="hashed-admin", user_id="default_user_id"),
)
await asyncio.sleep(0.05)
log = captured["log"]
assert log.object_id == hashed_token
assert raw_key not in (log.object_id or "")
assert raw_key not in (log.updated_values or "")
class TestRotateVirtualKeyInSecretManager:
"""Tests for _rotate_virtual_key_in_secret_manager with team_id support."""