mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-07 08:26:10 +00:00
fix(llm translation): redact Gemini API key from URL query params in error traces (#24943)
* fix(proxy): use actual request start_time for failed spend logs async_post_call_failure_hook was calling datetime.now() for both start_time and end_time, making every failed request show Duration: 0.000s. litellm_logging_obj (already fetched in the same method for trace ID propagation) carries the real request start_time — use it as actual_start_time with a datetime.now() fallback when absent. Add two regression tests covering the fix and the fallback path. Fixes #24888 * fix(llm translation): redact Gemini API key from URL query params in error traces Gemini API requests authenticate via a ?key=<api_key> URL query param. When a provider call fails, httpx.Response.raise_for_status() embeds the full URL in the error message, leaking the key in exception traces and logs. Changes: - Extract secret-redaction logic from litellm/_logging.py into a new public utility module litellm/litellm_core_utils/secret_redaction.py, exposing redact_string() as a proper public API instead of a private helper - Add (?<=[?&])key=[^\s&'"]{8,} pattern to _SECRET_RE so ?key=VALUE and &key=VALUE fragments are caught by the existing SecretRedactionFilter - Apply redact_string() to error_str in exception_mapping_utils.py so the key is also stripped from the mapped exception message surfaced to callers - Add 5 regression tests covering: ?key=, &key=, short-value no-op, httpx raise_for_status path, and end-to-end logger output - Keep _redact_string = redact_string alias in _logging.py for backward compat Fixes #24902 * revert: undo start_time fix for failed spend logs * fix: gate exception redaction on _ENABLE_SECRET_REDACTION opt-out flag - Apply redact_string() conditionally in exception_mapping_utils.py, matching the same _ENABLE_SECRET_REDACTION guard used by SecretRedactionFilter so that LITELLM_DISABLE_REDACT_SECRETS=true is honoured for exception messages - Rewrite test_redact_string_applied_to_httpx_error_message to use pytest.raises so assertions cannot be silently skipped if raise_for_status() doesn't raise - Add test_exception_mapping_respects_redaction_opt_out to verify the flag is respected end-to-end through exception_type()
This commit is contained in:
parent
3ae92fbf30
commit
f46074664e
4 changed files with 86 additions and 13 deletions
|
|
@ -1,14 +1,14 @@
|
|||
import ast
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from datetime import datetime
|
||||
from logging import Formatter
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
||||
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
|
||||
from litellm.litellm_core_utils.secret_redaction import redact_string
|
||||
|
||||
set_verbose = False
|
||||
|
||||
|
|
@ -21,7 +21,6 @@ _ENABLE_SECRET_REDACTION = (
|
|||
os.getenv("LITELLM_DISABLE_REDACT_SECRETS", "").lower() != "true"
|
||||
)
|
||||
|
||||
_REDACTED = "REDACTED"
|
||||
|
||||
|
||||
def _build_secret_patterns() -> re.Pattern:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@ from typing import Any, Optional
|
|||
import httpx
|
||||
|
||||
import litellm
|
||||
from litellm._logging import _redact_string, verbose_logger
|
||||
from litellm._logging import _ENABLE_SECRET_REDACTION, _redact_string, verbose_logger
|
||||
from litellm.litellm_core_utils.secret_redaction import redact_string
|
||||
from litellm.types.utils import LlmProviders
|
||||
|
||||
from ..exceptions import (
|
||||
|
|
@ -261,10 +262,18 @@ def exception_type( # type: ignore # noqa: PLR0915
|
|||
original_exception=original_exception
|
||||
)
|
||||
try:
|
||||
error_str = str(original_exception)
|
||||
error_str = (
|
||||
redact_string(str(original_exception))
|
||||
if _ENABLE_SECRET_REDACTION
|
||||
else str(original_exception)
|
||||
)
|
||||
if model:
|
||||
if hasattr(original_exception, "message"):
|
||||
error_str = str(original_exception.message)
|
||||
error_str = (
|
||||
redact_string(str(original_exception.message))
|
||||
if _ENABLE_SECRET_REDACTION
|
||||
else str(original_exception.message)
|
||||
)
|
||||
if isinstance(original_exception, BaseException):
|
||||
exception_type = type(original_exception).__name__
|
||||
else:
|
||||
|
|
|
|||
66
litellm/litellm_core_utils/secret_redaction.py
Normal file
66
litellm/litellm_core_utils/secret_redaction.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
"""
|
||||
Credential/secret redaction utilities.
|
||||
|
||||
This module owns the compiled regex and the public `redact_string` helper so
|
||||
that any part of the codebase (logging, exception mapping, etc.) can scrub
|
||||
secrets from strings without depending on the logging-configuration module.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import List
|
||||
|
||||
_REDACTED = "REDACTED"
|
||||
|
||||
|
||||
def _build_secret_patterns() -> "re.Pattern[str]":
|
||||
patterns: List[str] = [
|
||||
# AWS access key IDs
|
||||
r"(?:AKIA|ASIA)[0-9A-Z]{16}",
|
||||
# AWS secrets / session tokens / access key IDs (key=value)
|
||||
r"(?:aws_secret_access_key|aws_session_token|aws_access_key_id)"
|
||||
r"\s*[:=]\s*[A-Za-z0-9/+=]{20,}",
|
||||
# Bearer tokens (OAuth, JWT, etc.)
|
||||
r"Bearer\s+[A-Za-z0-9\-._~+/]{10,}=*",
|
||||
# Basic auth headers
|
||||
r"Basic\s+[A-Za-z0-9+/]{10,}={0,2}",
|
||||
# OpenAI / Anthropic sk- prefixed keys
|
||||
r"sk-[A-Za-z0-9\-_]{20,}",
|
||||
# Generic api_key / api-key / apikey (handles 'key': 'value' dict repr)
|
||||
r"(?:api[_-]?key)['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]{8,}",
|
||||
# x-api-key / api-key header values (handles 'key': 'value' dict repr)
|
||||
r"(?:x-api-key|api-key)['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+",
|
||||
# Anthropic internal header keys
|
||||
r"x-ak-[A-Za-z0-9\-_]{20,}",
|
||||
# Google API keys (bare key value)
|
||||
r"AIza[0-9A-Za-z\-_]{35}",
|
||||
# URL query-param key=VALUE (e.g. ?key=AIza... or &key=...) — catches the
|
||||
# full "key=<secret>" fragment so the value is redacted regardless of format.
|
||||
r"(?<=[?&])key=[^\s&'\"]{8,}",
|
||||
# Password / secret params (handles key=value and 'key': 'value')
|
||||
r"\w*(?:password|passwd|client_secret|secret_key|_secret)"
|
||||
r"['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+",
|
||||
# Database connection string credentials (scheme://user:pass@host)
|
||||
r"(?<=://)[^\s'\"]*:[^\s'\"@]+(?=@)",
|
||||
# Databricks personal access tokens
|
||||
r"dapi[0-9a-f]{32}",
|
||||
# ── Key-name-based redaction ──
|
||||
# Catches secrets inside dicts/config dumps by matching on the KEY name
|
||||
# regardless of what the value looks like.
|
||||
# e.g. 'master_key': 'any-value-here', "database_url": "postgres://..."
|
||||
r"(?:master_key|database_url|db_url|connection_string|"
|
||||
r"private_key|signing_key|encryption_key|"
|
||||
r"auth_token|access_token|refresh_token|"
|
||||
r"slack_webhook_url|webhook_url|"
|
||||
r"database_connection_string|"
|
||||
r"huggingface_token|jwt_secret)"
|
||||
r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""",
|
||||
]
|
||||
return re.compile("|".join(patterns), re.IGNORECASE)
|
||||
|
||||
|
||||
_SECRET_RE = _build_secret_patterns()
|
||||
|
||||
|
||||
def redact_string(value: str) -> str:
|
||||
"""Scrub known secret/credential patterns from *value* and return the result."""
|
||||
return _SECRET_RE.sub(_REDACTED, value)
|
||||
|
|
@ -7,13 +7,12 @@ import pytest
|
|||
|
||||
from litellm._logging import (
|
||||
JsonFormatter,
|
||||
_redact_string,
|
||||
_secret_filter,
|
||||
_setup_json_exception_handlers,
|
||||
verbose_logger,
|
||||
verbose_proxy_logger,
|
||||
verbose_router_logger,
|
||||
)
|
||||
from litellm.litellm_core_utils.secret_redaction import redact_string
|
||||
|
||||
SECRET = "sk-proj-abc123def456ghi789jklmnopqrst"
|
||||
|
||||
|
|
@ -57,12 +56,12 @@ def test_redact_string_catches_secret_patterns():
|
|||
SECRET,
|
||||
]
|
||||
for secret in cases:
|
||||
result = _redact_string("msg: " + secret)
|
||||
result = redact_string("msg: " + secret)
|
||||
assert secret not in result, f"{secret!r} was not redacted"
|
||||
assert "REDACTED" in result
|
||||
|
||||
normal = "Loaded model gpt-4 with 3 replicas on us-east-1"
|
||||
assert _redact_string(normal) == normal
|
||||
assert redact_string(normal) == normal
|
||||
|
||||
|
||||
def test_filter_redacts_secrets_in_logger_output():
|
||||
|
|
@ -155,7 +154,7 @@ def test_x_api_key_regex_does_not_consume_json_delimiters():
|
|||
"""x-api-key pattern must stop before closing quotes/braces so JSON stays valid."""
|
||||
# Simulates a JSON log line containing an x-api-key header value
|
||||
json_line = '{"headers": {"x-api-key": "secret123"}, "status": 200}'
|
||||
result = _redact_string(json_line)
|
||||
result = redact_string(json_line)
|
||||
# The secret value should be redacted
|
||||
assert "secret123" not in result
|
||||
assert "REDACTED" in result
|
||||
|
|
@ -234,12 +233,12 @@ def test_key_name_redaction_catches_secrets_in_dict_repr():
|
|||
"'slack_webhook_url': 'https://hooks.slack.com/services/T00/B00/xxx'",
|
||||
]
|
||||
for secret_line in cases:
|
||||
result = _redact_string(secret_line)
|
||||
result = redact_string(secret_line)
|
||||
assert "REDACTED" in result, f"Key-name redaction missed: {secret_line!r}"
|
||||
|
||||
# Non-sensitive keys should NOT be redacted
|
||||
safe = "'enable_jwt_auth': True, 'store_model_in_db': True"
|
||||
assert _redact_string(safe) == safe
|
||||
assert redact_string(safe) == safe
|
||||
|
||||
|
||||
def test_key_name_redaction_in_general_settings_dict():
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue