From 0bdbf8d7bda2b44731172314912109d3aaddabcf Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Sat, 30 May 2026 21:42:23 +0000 Subject: [PATCH] chore(proxy): fully redact audit values so short secrets cannot leak The audit reader masked via the default partial-reveal masker, which returns values of 8 chars or fewer verbatim, so a short secret in an audit snapshot escaped redaction. Use a no-reveal masker (visible_prefix/suffix=0) on the audit read path; an audit log is a change record, not a place to read a secret back from. Add masker- and audit-level regression tests for short values. --- .../proxy/audit_logging_endpoints.py | 5 ++++- .../litellm_core_utils/test_sensitive_data_masker.py | 12 ++++++++++++ .../proxy/test_readonly_admin_secret_redaction.py | 12 ++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py index 73d48ccf1cf..0ad27092950 100644 --- a/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py +++ b/enterprise/litellm_enterprise/proxy/audit_logging_endpoints.py @@ -23,7 +23,10 @@ from litellm.proxy.auth.user_api_key_auth import user_api_key_auth router = APIRouter() -_AUDIT_VALUE_MASKER = SensitiveDataMasker() +# Fully redact (no prefix/suffix reveal) so even short secrets in an audit +# snapshot are masked; the audit log is a change record, not a place to read a +# secret back from, so there is no value in revealing any of it. +_AUDIT_VALUE_MASKER = SensitiveDataMasker(visible_prefix=0, visible_suffix=0) def _redact_audit_log_values(audit_log_dict: Dict[str, Any]) -> Dict[str, Any]: diff --git a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py index f293d252d8d..9ae687abb21 100644 --- a/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py +++ b/tests/test_litellm/litellm_core_utils/test_sensitive_data_masker.py @@ -197,3 +197,15 @@ def test_mask_url_credentials_preserves_ipv6_brackets(): out = mask_url_credentials("redis://:supersecretpw@[::1]:6379") assert out == "redis://:****@[::1]:6379" assert "supersecretpw" not in out + + +def test_fully_redacting_masker_masks_short_secret_values(): + """A masker configured with no visible prefix/suffix (as used on the audit + read path) fully masks even short secrets, which the default partial-reveal + masker would otherwise return verbatim.""" + masker = SensitiveDataMasker(visible_prefix=0, visible_suffix=0) + masked = masker.mask_dict({"api_key": "sk12", "password": "x", "port": 6379}) + assert masked["api_key"] != "sk12" + assert set(masked["api_key"]) == {"*"} + assert masked["password"] == "*" + assert masked["port"] == 6379 diff --git a/tests/test_litellm/proxy/test_readonly_admin_secret_redaction.py b/tests/test_litellm/proxy/test_readonly_admin_secret_redaction.py index 6216da5118d..f5241c95f0d 100644 --- a/tests/test_litellm/proxy/test_readonly_admin_secret_redaction.py +++ b/tests/test_litellm/proxy/test_readonly_admin_secret_redaction.py @@ -97,6 +97,18 @@ def test_redact_audit_log_values_handles_string_and_non_dict_input(): ) +def test_redact_audit_log_values_masks_short_secrets(): + """A short secret in an audit snapshot must still be masked, not returned + verbatim because it falls under the masker's partial-reveal length.""" + from litellm_enterprise.proxy.audit_logging_endpoints import ( + _redact_audit_log_values, + ) + + out = _redact_audit_log_values({"updated_values": {"api_key": "sk12"}}) + assert out["updated_values"]["api_key"] != "sk12" + assert set(out["updated_values"]["api_key"]) == {"*"} + + # --------------------------------------------------------------------------- # # Pass-through endpoint header masking # --------------------------------------------------------------------------- #