From ebfd9d21e43bbd40c5365ae0c3b5acb3147d6955 Mon Sep 17 00:00:00 2001 From: yucheng-berri <293926326+yucheng-berri@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:51:25 -0700 Subject: [PATCH] fix(guardrails): cap the scanned key after JSON escaping A key of control characters escaped to six times its length after the cap, so every list item's scan line grew with it --- .../enterprise_callbacks/secret_detection.py | 2 +- .../security/test_tool_text_guardrail_consumers.py | 1 - .../enterprise_callbacks/test_secret_detection.py | 10 ++++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py b/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py index 40f25f6b36d..f5ae46eca3d 100644 --- a/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py +++ b/enterprise/litellm_enterprise/enterprise_callbacks/secret_detection.py @@ -548,7 +548,7 @@ def _json_unescaped(value: str) -> str: def _json_line(key: str | None, text: str) -> str: value: Final = json.dumps(text, ensure_ascii=False) - return value if key is None else f"{json.dumps(key[-_KEY_CONTEXT:], ensure_ascii=False)}: {value}" + return value if key is None else f'"{json.dumps(key, ensure_ascii=False)[1:-1][-_KEY_CONTEXT:]}": {value}' def _value_in_texts(value: str, texts: Sequence[str]) -> str | None: diff --git a/tests/integration/security/test_tool_text_guardrail_consumers.py b/tests/integration/security/test_tool_text_guardrail_consumers.py index 4a5c8afbbb1..ea90ad8ed04 100644 --- a/tests/integration/security/test_tool_text_guardrail_consumers.py +++ b/tests/integration/security/test_tool_text_guardrail_consumers.py @@ -218,4 +218,3 @@ def test_moderation_flag_in_tool_result_blocks(rig: Rig) -> None: response: Final = rig.proxy.request("POST", "/v1/messages", _messages(FLAGGED)) assert response.status_code == 403, response.text assert "Violated content safety policy" in response.text - assert _provider_calls(rig.wire.drain()) == () diff --git a/tests/unit/enterprise/enterprise_callbacks/test_secret_detection.py b/tests/unit/enterprise/enterprise_callbacks/test_secret_detection.py index f398046f602..11dc0504aba 100644 --- a/tests/unit/enterprise/enterprise_callbacks/test_secret_detection.py +++ b/tests/unit/enterprise/enterprise_callbacks/test_secret_detection.py @@ -159,6 +159,16 @@ def test_list_items_are_scanned_beside_their_parent_key_or_quoted(payload, expec assert _guardrail().redact_tool_payload(payload) == expected +@pytest.mark.parametrize("key", ("k" * 10_000, "\u0000" * 1000), ids=("long", "control-characters")) +def test_scanned_key_context_is_capped_after_escaping(monkeypatch, key): + guardrail = _guardrail() + scan = guardrail.scan_message_for_secrets + scanned = [] + monkeypatch.setattr(guardrail, "scan_message_for_secrets", lambda text: scanned.append(text) or scan(text)) + assert guardrail.redact_tool_payload({key: ["v", "w"]}) == {key: ["v", "w"]} + assert max(len(line) for text in scanned for line in text.splitlines()) == 64 + len('"": "v"') + + def test_long_key_over_a_list_is_scanned_in_bounded_time(): payload = {"k" * 10_000: ["v"] * 1000 + [f"AWS_ACCESS_KEY_ID={AWS_KEY}"]} started = time.perf_counter()