From 13b0cbb7210d19402774c76f9f251eb9132e5ec7 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Fri, 13 Mar 2026 22:28:37 -0700 Subject: [PATCH] fix: redact secrets in JSON exception handlers, fix x-api-key regex consuming delimiters - Add _secret_filter to _setup_json_exception_handlers error_handler so unhandled exceptions in JSON mode get secrets redacted - Fix async exception handler to pass real exc_info tuple instead of None so tracebacks are captured and redacted - Change x-api-key regex from \S+ to bounded character class to stop before closing quotes/braces that would corrupt JSON log lines - Add tests for all three fixes --- litellm/_logging.py | 6 +- tests/test_litellm/test_secret_redaction.py | 67 +++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/litellm/_logging.py b/litellm/_logging.py index d964092e10c..5de9fbb3558 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -38,7 +38,7 @@ def _build_secret_patterns() -> re.Pattern: # 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+", + r"(?:x-api-key|api-key)['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+", # Anthropic internal header keys r"x-ak-[A-Za-z0-9\-_]{20,}", # Google API keys @@ -209,6 +209,7 @@ def _setup_json_exception_handlers(formatter): # Create a handler with JSON formatting for exceptions error_handler = logging.StreamHandler() error_handler.setFormatter(formatter) + error_handler.addFilter(_secret_filter) # Setup excepthook for uncaught exceptions def json_excepthook(exc_type, exc_value, exc_traceback): @@ -232,6 +233,7 @@ def _setup_json_exception_handlers(formatter): def async_json_exception_handler(loop, context): exception = context.get("exception") if exception: + exc_type = type(exception) record = logging.LogRecord( name="LiteLLM", level=logging.ERROR, @@ -239,7 +241,7 @@ def _setup_json_exception_handlers(formatter): lineno=0, msg=str(exception), args=(), - exc_info=None, + exc_info=(exc_type, exception, exception.__traceback__), ) error_handler.handle(record) else: diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index d2c74afc3b4..821529c111b 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -1,12 +1,15 @@ 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, @@ -146,3 +149,67 @@ def test_disable_redaction_passes_secrets_through(): ) _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