fix: redact secrets in exception tracebacks and extra fields, normalize env var check

- Redact exc_info tracebacks so secrets in exception messages don't leak
- Redact string values in extra={} fields passed to loggers
- Use case-insensitive check for LITELLM_REDACT_SECRETS env var (consistent with rest of codebase)
- Remove unused Union import
This commit is contained in:
Ryan Crabbe 2026-03-12 09:06:00 -07:00
parent 9173c1abce
commit bd16532431
2 changed files with 54 additions and 2 deletions

View file

@ -5,7 +5,7 @@ import re
import sys
from datetime import datetime
from logging import Formatter
from typing import Any, Dict, List, Optional, Union
from typing import Any, Dict, List, Optional
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
from litellm.litellm_core_utils.safe_json_loads import safe_json_loads
@ -17,7 +17,7 @@ if set_verbose is True:
"`litellm.set_verbose` is deprecated. Please set `os.environ['LITELLM_LOG'] = 'DEBUG'` for debug logs."
)
_ENABLE_SECRET_REDACTION = os.getenv("LITELLM_REDACT_SECRETS") == "TRUE"
_ENABLE_SECRET_REDACTION = os.getenv("LITELLM_REDACT_SECRETS", "").lower() == "true"
_REDACTED = "REDACTED"
@ -64,6 +64,8 @@ def _redact_string(value: str) -> str:
class SecretRedactionFilter(logging.Filter):
"""Scrubs known secret/credential patterns from log records."""
_formatter = logging.Formatter()
def filter(self, record: logging.LogRecord) -> bool:
if not _ENABLE_SECRET_REDACTION:
return True
@ -75,6 +77,20 @@ class SecretRedactionFilter(logging.Filter):
if isinstance(record.msg, str):
record.msg = _redact_string(record.msg)
# Redact exception tracebacks
if record.exc_info and record.exc_info[1] is not None:
try:
record.exc_text = _redact_string(
self._formatter.formatException(record.exc_info)
)
except Exception:
pass
# Redact extra fields passed via logger.debug("msg", extra={...})
for key, value in record.__dict__.items():
if key not in _STANDARD_RECORD_ATTRS and isinstance(value, str):
setattr(record, key, _redact_string(value))
return True

View file

@ -96,6 +96,42 @@ def test_filter_redacts_non_string_args():
assert "REDACTED" in output
def test_filter_redacts_exception_tracebacks():
"""Secrets embedded in exception messages must be redacted in tracebacks."""
def log_messages():
try:
raise ValueError(f"Auth failed with key {SECRET}")
except ValueError:
verbose_logger.exception("Something went wrong")
output = _capture_logger_output(log_messages)
assert SECRET not in output
assert "REDACTED" in output
assert "Something went wrong" in output
def test_filter_redacts_extra_fields():
"""Secrets passed via extra={...} must be redacted on the record."""
record = logging.LogRecord(
name="test",
level=logging.DEBUG,
pathname="",
lineno=0,
msg="request completed",
args=(),
exc_info=None,
)
record.api_key = SECRET
record.region = "us-east-1"
_secret_filter.filter(record)
assert SECRET not in record.api_key
assert "REDACTED" in record.api_key
assert record.region == "us-east-1"
def test_disable_redaction_passes_secrets_through():
"""When _ENABLE_SECRET_REDACTION is False, secrets pass through."""
with patch("litellm._logging._ENABLE_SECRET_REDACTION", False):