From 50c7f107e1a4e018c67d79f2fc865311986d846d Mon Sep 17 00:00:00 2001 From: Deepanshu Date: Thu, 27 Aug 2026 18:20:10 -0400 Subject: [PATCH] fix(guardrails): block during_call when PII can't be safely masked Greptile finding (P1, security): async_moderation_hook's inject_system_message branch had no equivalent to async_pre_call_hook's degrade-to-blocking case for a PII violation on input that can't be safely masked (e.g. combined messages+input). It fell through to the advisory no-op branch and let raw, unredacted PII reach the model with no protection at all. Raising still blocks the response from reaching the caller even though during_call races with the LLM dispatch, the same mechanism on_flagged="block" already relies on for this hook, so add the same block-instead-of-advisory branch pre_call already has. --- .../guardrail_hooks/lakera_ai_v2.py | 9 +++++ .../guardrail_hooks/test_lakera_ai_v2.py | 40 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py index 904bceb011b..7ad5072f0ac 100644 --- a/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py +++ b/litellm/proxy/guardrails/guardrail_hooks/lakera_ai_v2.py @@ -668,6 +668,15 @@ class LakeraAIGuardrail(CustomGuardrail): _apply_redacted_messages_back_preserving_fields(self, data, redacted_messages) verbose_proxy_logger.debug("Lakera AI: Masked PII in messages instead of blocking request") elif self.on_flagged == "inject_system_message": + if _breakdown_has_pii_violation(lakera_guardrail_response) and is_multimodal_input: + # Same as async_pre_call_hook: there's PII in the mix and + # nothing here can be safely masked, so allowing it through + # unprotected would be worse than blocking. Unlike mutating + # data["messages"] below, raising still blocks the response + # from reaching the caller even though during_call races with + # the LLM dispatch -- same mechanism on_flagged="block" already + # relies on for this hook. + raise self._get_http_exception_for_blocked_guardrail(lakera_guardrail_response) if _breakdown_has_pii_violation(lakera_guardrail_response) and not is_multimodal_input: # A mixed violation (PII plus something else): mask whatever's # maskable even though the advisory note below has no effect diff --git a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py index f5d87bfa1ac..ca7dcc51a8e 100644 --- a/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py +++ b/tests/test_litellm/proxy/guardrails/guardrail_hooks/test_lakera_ai_v2.py @@ -1370,6 +1370,46 @@ class TestAdvisoryModeWiring: assert result["messages"][0]["content"] != original_content assert len(result["messages"]) == 1, "no advisory note is appended during during_call" + @pytest.mark.asyncio + async def test_moderation_hook_blocks_instead_of_advisory_when_pii_is_not_maskable(self): + """ + Greptile finding (P1, security) on BerriAI/litellm#34940: a PII violation + on input that can't be safely masked (combined messages+input) fell + through to the during_call no-op branch and let raw, unredacted PII reach + the model with no protection at all. async_pre_call_hook already degrades + to blocking for this exact case (see + test_pre_call_blocks_instead_of_advisory_when_pii_is_not_maskable) -- + async_moderation_hook must too, since raising here still blocks the + response from reaching the caller (same mechanism on_flagged="block" + already relies on), unlike mutating data["messages"] which races with + the concurrent LLM dispatch. + """ + lakera_guardrail = LakeraAIGuardrail(api_key="test_key", on_flagged="inject_system_message") + mock_response = { + "flagged": True, + "payload": [{"detector_type": "pii/email", "start": 11, "end": 26, "message_id": 0}], + "breakdown": [{"detector_type": "pii/email", "detected": True}], + } + + with patch.object(lakera_guardrail, "call_v2_guard", new_callable=AsyncMock) as mock_call: + mock_call.return_value = (mock_response, {}) + data = { + "messages": [{"role": "user", "content": "My email is test@example.com"}], + "input": "responses-api content", + "model": "gpt-5-mini", + "metadata": {}, + } + with pytest.raises(HTTPException): + await lakera_guardrail.async_moderation_hook( + data=data, + user_api_key_dict=UserAPIKeyAuth(api_key="test_key"), + call_type="completion", + ) + + assert data["messages"][0]["content"] == "My email is test@example.com", ( + "the raw content must be untouched, not partially rewritten before the block" + ) + class TestAdvisoryModePostCall: """