From 70c90e40613cd391f4feb712349b5dd57e8ad66b Mon Sep 17 00:00:00 2001 From: AlSh007 <85677626+AlSh007@users.noreply.github.com> Date: Sat, 17 Oct 2026 20:23:47 +0530 Subject: [PATCH] fix(masker): hide values past the depth cap instead of returning them unmasked mask_dict and _mask_sequence returned their input verbatim at the cap, so a secret nested deeper than that was not masked. Return REDACTED values there, as mask_credentials_in_payload does. Co-Authored-By: Claude Sonnet 5.5 Signed-off-by: AlSh007 <85677626+AlSh007@users.noreply.github.com> --- .../sensitive_data_masker.py | 6 ++++-- .../test_sensitive_data_masker.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/litellm/litellm_core_utils/sensitive_data_masker.py b/litellm/litellm_core_utils/sensitive_data_masker.py index 52758e617d8..d5efc7c6704 100644 --- a/litellm/litellm_core_utils/sensitive_data_masker.py +++ b/litellm/litellm_core_utils/sensitive_data_masker.py @@ -103,7 +103,8 @@ class SensitiveDataMasker: ) -> Sequence[object]: masked_items: Final[list[object]] = [] if depth >= max_depth: - return values + # The cap bounds the walk, not who may see a secret: hide what it did not inspect. + return [REDACTED for _ in values] for item in values: if isinstance(item, Mapping): @@ -124,7 +125,8 @@ class SensitiveDataMasker: excluded_keys: set[str] | None = None, ) -> dict[str, Any]: if depth >= max_depth: - return data + # The cap bounds the walk, not who may see a secret: hide what it did not inspect. + return {key: REDACTED for key in data} masked_data: Final[dict[str, object]] = {} for k, v in data.items(): diff --git a/tests/unit/litellm_core_utils/test_sensitive_data_masker.py b/tests/unit/litellm_core_utils/test_sensitive_data_masker.py index 2182620164c..4a48eb4355a 100644 --- a/tests/unit/litellm_core_utils/test_sensitive_data_masker.py +++ b/tests/unit/litellm_core_utils/test_sensitive_data_masker.py @@ -400,6 +400,26 @@ def test_mask_credentials_in_payload_masks_a_root_model_string_as_one_string(): assert result["api_key"].startswith("sk-r") +@pytest.mark.parametrize("wrap", ["mapping", "sequence"]) +def test_mask_dict_does_not_return_secrets_nested_past_the_depth_cap(wrap): + """The depth cap bounds the walk, not who may see a secret: a credential buried deeper than + the cap must come back hidden, as `mask_credentials_in_payload` does, and not verbatim.""" + from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + from litellm.litellm_core_utils.secret_redaction import REDACTED + + secret: Final = "sk-deep-1234567890abcdef" + node: dict[str, object] = {"api_key": secret, "model": "gpt-5.4-mini"} + # A list costs one more level than a mapping, so push both shapes well past the cap. + for _ in range(2 * DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER + 1): + node = {"extra_body": node} if wrap == "mapping" else {"providers": [node]} + + result: Final = SensitiveDataMasker().mask_dict({**node, "max_tokens": 17}) + + assert secret not in str(result) + assert REDACTED in str(result) + assert result["max_tokens"] == 17 + + def test_mask_credentials_in_payload_hides_containers_past_the_depth_cap(): from litellm.constants import DEFAULT_MAX_RECURSE_DEPTH_SENSITIVE_DATA_MASKER from litellm.litellm_core_utils.secret_redaction import REDACTED