From c34d78191bf69091ad46c87689d712ea36c7f571 Mon Sep 17 00:00:00 2001 From: BillionClaw Date: Tue, 17 Mar 2026 15:28:15 +0800 Subject: [PATCH] fix(key_management): redact sensitive callback credentials in key info response When dynamically enabling Langfuse callbacks at the key level, the credentials were being stored in plaintext in the key's metadata. When the /key/info endpoint was called, these credentials were returned in plaintext, exposing them to team members. This fix adds _sanitize_key_metadata_for_response() which redacts sensitive callback fields (like langfuse_secret_key, langfuse_public_key, etc.) from the metadata before returning it in API responses. Non-sensitive fields like langfuse_host and gcs_bucket_name are preserved. Fixes #23776 --- .../key_management_endpoints.py | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 1c0c212b60b..cab36f9cf7b 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -91,6 +91,7 @@ from litellm.types.router import Deployment from litellm.types.utils import ( BudgetConfig, PersonalUIKeyGenerationConfig, + StandardCallbackDynamicParams, TeamUIKeyGenerationConfig, ) @@ -2525,6 +2526,8 @@ async def info_key_fn_v2( except Exception: # if using pydantic v1 k = k.dict() + # Sanitize metadata to prevent leaking sensitive callback credentials + k = _sanitize_key_metadata_for_response(k) filtered_key_info.append(k) return {"key": data.keys, "info": filtered_key_info} @@ -2608,6 +2611,9 @@ async def info_key_fn( key_info = key_info.dict() key_info.pop("token") + # Sanitize metadata to prevent leaking sensitive callback credentials + key_info = _sanitize_key_metadata_for_response(key_info) + # Attach object_permission if object_permission_id is set key_info = await attach_object_permission_to_dict(key_info, prisma_client) @@ -5051,6 +5057,68 @@ async def key_health( ) +def _sanitize_key_metadata_for_response(key_info: Dict[str, Any]) -> Dict[str, Any]: + """ + Sanitize key metadata for API responses by redacting sensitive callback credentials. + + When key-level logging is configured, the metadata contains callback_vars with sensitive + credentials (langfuse_secret_key, langfuse_public_key, etc.). This function redacts those + values to prevent credential leakage via the /key/info endpoint. + + Args: + key_info: Dictionary containing key information, including metadata + + Returns: + Dictionary with sanitized metadata - sensitive credential values are replaced with "REDACTED" + """ + if not isinstance(key_info, dict): + return key_info + + # Create a deep copy to avoid modifying the original + sanitized = copy.deepcopy(key_info) + + # Get sensitive field names from StandardCallbackDynamicParams + sensitive_fields = set(StandardCallbackDynamicParams.__annotations__.keys()) + + # Remove non-sensitive fields that might be in StandardCallbackDynamicParams + non_sensitive_fields = { + "langfuse_host", + "langfuse_prompt_version", + "gcs_bucket_name", + "langsmith_project", + "langsmith_base_url", + "langsmith_sampling_rate", + "langsmith_tenant_id", + "posthog_api_url", + "weave_project_id", + "turn_off_message_logging", + "litellm_disabled_callbacks", + } + sensitive_fields = sensitive_fields - non_sensitive_fields + + metadata = sanitized.get("metadata") + if metadata and isinstance(metadata, dict): + # Sanitize logging configuration in metadata + logging_config = metadata.get("logging") + if logging_config and isinstance(logging_config, list): + sanitized_logging = [] + for callback in logging_config: + if isinstance(callback, dict) and "callback_vars" in callback: + # Create a copy of callback with redacted sensitive values + sanitized_callback = copy.deepcopy(callback) + callback_vars = sanitized_callback.get("callback_vars", {}) + if isinstance(callback_vars, dict): + for field in sensitive_fields: + if field in callback_vars: + sanitized_callback["callback_vars"][field] = "REDACTED" + sanitized_logging.append(sanitized_callback) + else: + sanitized_logging.append(callback) + sanitized["metadata"]["logging"] = sanitized_logging + + return sanitized + + async def _can_user_query_key_info( user_api_key_dict: UserAPIKeyAuth, key: Optional[str],