litellm/tests/test_litellm/test_secret_redaction.py
Ryan Crabbe 47ddd0d0bf fix: redact secrets from proxy log output
Add SecretRedactionFilter to scrub API keys, tokens, and credentials
from all log records (messages, args, tracebacks, extra fields).

- Enable redaction by default; opt out with LITELLM_DISABLE_REDACT_SECRETS=true
- Redact patterns: sk-*, Bearer tokens, x-api-key values, base64 creds
- Handle JSON formatter exception hooks and percent-style format args
- Snapshot dict iteration to avoid RuntimeError during concurrent logging
2026-03-14 15:52:31 -07:00

215 lines
6.4 KiB
Python

import logging
import sys
from io import StringIO
from unittest.mock import patch
import pytest
from litellm._logging import (
JsonFormatter,
_redact_string,
_secret_filter,
_setup_json_exception_handlers,
verbose_logger,
verbose_proxy_logger,
verbose_router_logger,
)
SECRET = "sk-proj-abc123def456ghi789jklmnopqrst"
@pytest.fixture(autouse=True)
def _enable_redaction():
"""Ensure secret redaction is on (the default) for all tests in this module."""
with patch("litellm._logging._ENABLE_SECRET_REDACTION", True):
yield
def _capture_logger_output(fn):
"""Run fn with all litellm loggers wired to a StringIO buffer, return output."""
buf = StringIO()
h = logging.StreamHandler(buf)
h.addFilter(_secret_filter)
loggers = [verbose_logger, verbose_proxy_logger, verbose_router_logger]
saved = [(lg, lg.handlers[:], lg.level) for lg in loggers]
for lg in loggers:
lg.handlers.clear()
lg.addHandler(h)
lg.setLevel(logging.DEBUG)
try:
fn()
return buf.getvalue()
finally:
for lg, handlers, level in saved:
lg.handlers.clear()
for old_h in handlers:
lg.addHandler(old_h)
lg.setLevel(level)
def test_redact_string_catches_secret_patterns():
"""Core regex patterns redact known secret formats."""
cases = [
"Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig",
"api_key=a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
"password=supersecretpassword123",
"postgresql://admin:s3cretpass@db.example.com:5432/mydb",
SECRET,
]
for secret in cases:
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
def test_filter_redacts_secrets_in_logger_output():
def log_messages():
verbose_logger.debug("Key: " + SECRET)
verbose_logger.debug("Normal message with no secrets")
output = _capture_logger_output(log_messages)
assert SECRET not in output
assert "REDACTED" in output
assert "Normal message with no secrets" in output
def test_filter_redacts_percent_style_args():
"""Secrets passed as %-style args should be redacted."""
def log_messages():
verbose_logger.debug("key=%s region=%s", SECRET, "us-east-1")
output = _capture_logger_output(log_messages)
assert SECRET not in output
assert "us-east-1" in output
def test_filter_redacts_non_string_args():
"""Secrets inside dicts/lists passed as %-style args should be redacted."""
def log_messages():
verbose_logger.debug("Config: %s", {"nested": {"key": SECRET}})
verbose_logger.debug("Keys: %s", [SECRET])
output = _capture_logger_output(log_messages)
assert SECRET not in output
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 LITELLM_DISABLE_REDACT_SECRETS=true, secrets pass through."""
with patch("litellm._logging._ENABLE_SECRET_REDACTION", False):
record = logging.LogRecord(
name="test",
level=logging.DEBUG,
pathname="",
lineno=0,
msg="key=" + SECRET,
args=(),
exc_info=None,
)
_secret_filter.filter(record)
assert "sk-proj-" in record.msg
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)
# The secret value should be redacted
assert "secret123" not in result
assert "REDACTED" in result
# Closing delimiter must survive so the line is still valid-ish JSON
assert '"status": 200' in result
assert "}" in result
def test_json_excepthook_redacts_secrets():
"""Unhandled exceptions in JSON mode must have secrets redacted."""
buf = StringIO()
h = logging.StreamHandler(buf)
h.setFormatter(JsonFormatter())
h.addFilter(_secret_filter)
# Capture what the excepthook would emit
record = logging.LogRecord(
name="LiteLLM",
level=logging.ERROR,
pathname="",
lineno=0,
msg=f"Connection failed with key {SECRET}",
args=(),
exc_info=None,
)
# Simulate the filter + formatter pipeline
_secret_filter.filter(record)
output = h.formatter.format(record)
assert SECRET not in output
assert "REDACTED" in output
def test_json_excepthook_redacts_traceback_secrets():
"""Unhandled exception tracebacks in JSON mode must have secrets redacted."""
buf = StringIO()
h = logging.StreamHandler(buf)
h.setFormatter(JsonFormatter())
h.addFilter(_secret_filter)
try:
raise RuntimeError(f"Failed to auth with {SECRET}")
except RuntimeError:
exc_info = sys.exc_info()
record = logging.LogRecord(
name="LiteLLM",
level=logging.ERROR,
pathname="",
lineno=0,
msg=str(exc_info[1]),
args=(),
exc_info=exc_info,
)
_secret_filter.filter(record)
output = h.formatter.format(record)
assert SECRET not in output
assert "REDACTED" in output