diff --git a/litellm/_logging.py b/litellm/_logging.py index 18c3bcb7e87..65e6045b0b1 100644 --- a/litellm/_logging.py +++ b/litellm/_logging.py @@ -52,6 +52,17 @@ def _build_secret_patterns() -> re.Pattern: 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) @@ -272,7 +283,7 @@ def async_json_exception_handler(loop, context): verbose_router_logger = logging.getLogger("LiteLLM Router") verbose_logger = logging.getLogger("LiteLLM") -# Add the handler to the logger +# Add the handler to the loggers verbose_router_logger.addHandler(handler) verbose_proxy_logger.addHandler(handler) verbose_logger.addHandler(handler) diff --git a/tests/test_litellm/test_secret_redaction.py b/tests/test_litellm/test_secret_redaction.py index 821529c111b..1d575195194 100644 --- a/tests/test_litellm/test_secret_redaction.py +++ b/tests/test_litellm/test_secret_redaction.py @@ -213,3 +213,52 @@ def test_json_excepthook_redacts_traceback_secrets(): output = h.formatter.format(record) assert SECRET not in output assert "REDACTED" in output + + +def test_key_name_redaction_catches_secrets_in_dict_repr(): + """Secrets inside dict repr strings are redacted based on key names.""" + cases = [ + # Python dict repr (the exact leak format from the bug report) + "param_name=general_settings, param_value={'master_key': 'my-random-secret-key-1234', 'enable_jwt_auth': True}", + # database_url + "'database_url': 'postgres://admin:password@db.example.com:5432/litellm'", + # JSON format + '"database_url": "postgres://admin:password@db.example.com:5432/litellm"', + # access_token + "'access_token': 'some-opaque-token-value'", + # refresh_token + "refresh_token=my-refresh-tok-12345", + # auth_token + "'auth_token': 'random-auth-value'", + # slack_webhook_url + "'slack_webhook_url': 'https://hooks.slack.com/services/T00/B00/xxx'", + ] + for secret_line in cases: + 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 + + +def test_key_name_redaction_in_general_settings_dict(): + """End-to-end: secrets inside a general_settings dict dump are redacted + when logged through the named litellm loggers.""" + + def log_messages(): + general_settings = { + "master_key": "my-random-secret-key-1234", + "database_url": "postgres://admin:password@db.example.com:5432/litellm", + "enable_jwt_auth": True, + "store_model_in_db": True, + } + verbose_proxy_logger.debug( + f"param_name=general_settings, param_value={general_settings}" + ) + + output = _capture_logger_output(log_messages) + assert "my-random-secret-key-1234" not in output + assert "REDACTED" in output + # Non-sensitive values should survive + assert "enable_jwt_auth" in output