fix(masker): hide containers at the redaction depth limit instead of passing them through

This commit is contained in:
mateo-berri 2026-09-03 03:12:04 -07:00
parent 912572bfa5
commit 3abed5f4c9
2 changed files with 29 additions and 8 deletions

View file

@ -226,30 +226,30 @@ def redact_credentials_in_payload(data: Mapping[str, object]) -> Mapping[str, ob
and non-string secrets are covered too, which is what a payload rendered
straight to stdout needs. ``None`` is preserved so an unset credential still
reads as unset, and lists and tuples are rebuilt element by element so a
credential nested inside one is caught as well.
credential nested inside one is caught as well. A container sitting at the
recursion limit is replaced wholesale rather than passed through, so nesting a
payload deeper than the limit hides it instead of exposing it.
"""
return _redact_mapping(data, 0)
def _redact_mapping(data: Mapping[str, object], depth: int) -> Mapping[str, object]:
if depth >= DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER:
return data
return {key: _redact_entry(key, value, depth) for key, value in data.items()}
def _redact_entry(key: str, value: object, depth: int) -> object:
if value is not None and _default_masker.is_sensitive_key(key):
return REDACTED
if not isinstance(value, (Mapping, list, tuple)):
return value
if depth >= DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER:
return REDACTED
if isinstance(value, Mapping):
return _redact_mapping(value, depth + 1)
if isinstance(value, (list, tuple)):
return _redact_sequence(value, depth + 1)
return value
return _redact_sequence(value, depth + 1)
def _redact_sequence(values: Sequence[object], depth: int) -> Sequence[object]:
if depth >= DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER:
return values
redacted: Final = tuple(_redact_entry("", item, depth) for item in values)
return redacted if isinstance(values, tuple) else list(redacted)

View file

@ -372,3 +372,24 @@ def test_redact_credentials_in_payload_reaches_credentials_nested_in_sequences()
assert result["metadata"]["upstreams"][0]["aws_secret_access_key"] == "REDACTED"
assert isinstance(result["metadata"]["upstreams"], tuple)
assert result["messages"] == [{"role": "user", "content": "hello"}]
@pytest.mark.parametrize("wrap", ["mapping", "sequence"])
def test_redact_credentials_in_payload_hides_containers_at_the_recursion_limit(wrap):
"""The recursion limit exists to bound the walk, not to grant an exemption, so a caller who
buries a credential deeper than the limit must get the container hidden rather than handed
back verbatim. Nesting through lists costs depth twice as fast as nesting through mappings,
so both shapes are pushed well past the limit here."""
from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER
from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload
fake_key = "sk-fake-lit6835-past-the-limit"
node = {"api_key": fake_key}
for _ in range(2 * DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + 1):
node = {"extra_body": node} if wrap == "mapping" else {"providers": [node]}
result = redact_credentials_in_payload({**node, "max_tokens": 17})
assert fake_key not in str(result)
assert "REDACTED" in str(result)
assert result["max_tokens"] == 17