fix(proxy): redact body snapshot on pre-call guardrail failure to protect spend logs (#50127)

This commit is contained in:
Harshit Kulkarni 2026-11-05 16:12:29 +05:30
parent 79e63d3e06
commit da3176cceb
3 changed files with 63 additions and 6 deletions

View file

@ -286,6 +286,7 @@ from litellm.proxy.litellm_pre_call_utils import (
LiteLLMProxyRequestSetup,
add_litellm_data_to_request,
get_metadata_variable_name,
redact_proxy_server_request_body_on_guardrail_block,
refresh_proxy_server_request_body_snapshot,
reject_url_valued_destination,
sanitize_for_log,
@ -2670,12 +2671,17 @@ class ProxyBaseLLMRequestProcessing:
prefetch_model = self.data.get("model")
if llm_router is not None and isinstance(prefetch_model, str):
RoutingContext(llm_router).arm_read_prefetch(prefetch_model, self.data)
self.data = await proxy_logging_obj.pre_call_hook(
user_api_key_dict=user_api_key_dict,
data=self.data,
call_type=route_type,
skip_guardrails=skip_guardrails,
)
try:
self.data = await proxy_logging_obj.pre_call_hook(
user_api_key_dict=user_api_key_dict,
data=self.data,
call_type=route_type,
skip_guardrails=skip_guardrails,
)
except Exception:
if isinstance(self.data, dict):
redact_proxy_server_request_body_on_guardrail_block(self.data)
raise
await _enforce_guardrail_added_tag_budgets(
data=self.data,
tags_before_guardrails=self._tags_before_guardrails,

View file

@ -2608,6 +2608,26 @@ def refresh_proxy_server_request_body_snapshot(
)
def redact_proxy_server_request_body_on_guardrail_block(
data: MutableMapping[str, object],
) -> None:
"""
Redact prompt/messages fields in data['proxy_server_request']['body'] when a pre-call
guardrail blocks a request (#50127).
This ensures that raw PII or secret content that triggered a guardrail rejection is not
persisted in plain text in LiteLLM_SpendLogs via async_post_call_failure_hook.
"""
proxy_server_request: Final = data.get("proxy_server_request")
if not isinstance(proxy_server_request, dict):
return
body: Final = proxy_server_request.get("body")
if not isinstance(body, dict):
return
for field in ("messages", "prompt", "input"):
if field in body:
body[field] = "[REDACTED - BLOCKED BY GUARDRAIL]"
async def add_litellm_data_to_request(
data: dict,
request: Request,

View file

@ -1391,6 +1391,37 @@ def test_refresh_proxy_server_request_body_snapshot_picks_up_guardrail_masking()
assert "123-45-6789" not in json.dumps(refreshed_body)
def test_redact_proxy_server_request_body_on_guardrail_block():
"""
Regression: When a pre-call guardrail blocks a request (e.g. secret or PII detected),
pre_call_hook raises an exception. The raw snapshot in proxy_server_request['body']
must be redacted so raw blocked text is not written into SpendLogs in plain text (#50127).
"""
from litellm.proxy.litellm_pre_call_utils import (
redact_proxy_server_request_body_on_guardrail_block,
)
data = {
"model": "gpt-4",
"messages": [{"role": "user", "content": "secret password is supersecret"}],
"proxy_server_request": {
"url": "http://localhost/v1/chat/completions",
"body": {
"model": "gpt-4",
"messages": [{"role": "user", "content": "secret password is supersecret"}],
"prompt": "supersecret prompt",
},
},
}
redact_proxy_server_request_body_on_guardrail_block(data)
body = data["proxy_server_request"]["body"]
assert body["messages"] == "[REDACTED - BLOCKED BY GUARDRAIL]"
assert body["prompt"] == "[REDACTED - BLOCKED BY GUARDRAIL]"
assert "supersecret" not in json.dumps(body)
@pytest.mark.asyncio
async def test_add_litellm_data_to_request_strips_string_encoded_admin_injection():
"""Regression: metadata arriving as a JSON string (multipart/form-data or