fix(guardrails): pii/pci guardrail masking gaps in SpendLogs, debug logs, and logging_only response - #37965
Conversation
|
|
Greptile SummaryThe current head completes the audit-snapshot fix by excluding the live logging object while retaining the post-guardrail request state.
Confidence Score: 5/5The PR appears safe to merge. The previously reported audit-snapshot issue is fixed at the current head: the refreshed body explicitly excludes litellm_logging_obj, and regression tests verify both its absence and JSON serializability, so no blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/proxy/common_request_processing.py | Refreshes and logs request data after pre-call guardrails, ensuring the audit snapshot reflects masked content. |
| litellm/proxy/litellm_pre_call_utils.py | Centralizes audit-body snapshot creation and excludes secrets, transport credentials, self-reference, and the live logging object. |
| litellm/proxy/guardrails/guardrail_hooks/presidio.py | Extends logging-only masking to supported model and Anthropic response shapes before callback dispatch. |
| tests/test_litellm/proxy/test_common_request_processing.py | Verifies post-guardrail snapshot refresh, logging-object exclusion, and JSON serializability. |
| tests/test_litellm/proxy/test_litellm_pre_call_utils.py | Covers masked snapshot reconstruction and all required exclusions. |
| tests/test_litellm/proxy/guardrails/guardrail_hooks/test_presidio.py | Verifies that logging-only response handling masks sensitive response content. |
Reviews (2): Last reviewed commit: "fix(guardrails): stop PII/PCI masking ga..." | Re-trigger Greptile
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
…and logging_only response The Presidio guardrail masks messages in place inside pre_call_hook, but three paths independently persisted or emitted the raw pre-guardrail data: the SpendLogs proxy_server_request body snapshot (taken before the hook runs), a verbose_proxy_logger.debug dump of the raw request, and logging_only mode's async_logging_hook, which never masked the model's response before it reached external logging callbacks. Resolves LIT-6015
6b70093 to
ed8957e
Compare
|
@greptileai the runtime logging-object leak is fixed: |
9349b22
into
litellm_internal_staging
TLDR
Problem this solves:
How it solves it:
User Flow
Before: an admin turns on the PII masking guardrail expecting no raw PII/PCI to reach storage or logs, but it still does
pii-maskingguardrail and setsstore_prompts_in_spend_logs: truePOST https://litellm-domain/v1/chat/completionswith"My SSN is 123-45-6789 and my email is jane.doe@example.com"--detailed_debugon, the proxy's own debug log also prints the raw emailAfter: the same setup, but nothing raw ever reaches storage or logs
pii-maskingguardrail and setsstore_prompts_in_spend_logs: truePOST https://litellm-domain/v1/chat/completionsrequest with the same PII<EMAIL_ADDRESS>tokenRelevant issues
Linear ticket
Resolves LIT-6015
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*,make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more@greptileaito re-request a review after pushing changes)Delays in PR merge?
If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).
Screenshots / Proof of Fix
Setup for every case below: a live proxy on port 26018, a real Postgres SpendLogs table, real
presidio-analyzer/presidio-anonymizercontainers,general_settings.store_prompts_in_spend_logs: true, and apii-maskingPresidio guardrail (mode: pre_call,default_on: true) pointed at them. Every request below hit the real OpenAI API and cost real money.Before (7a1afa1)
SpendLogs audit trail still holds the raw email
"OK"(prompt_tokens=33, confirming Presidio really masked the outbound call).docker exec pg psql -U litellm -d litellm -tAc 'SELECT proxy_server_request FROM "LiteLLM_SpendLogs" LIMIT 1;' | grep -o "jane.doe@example.com\|<EMAIL_ADDRESS>"->jane.doe@example.combody.messagesstill contains the raw email, even though the model itself never received it.--detailed_debuglog leaks the raw email--detailed_debug.grep -n "receiving data" proxy.log-> one line fromlitellm_pre_call_utils.py:1805, printed before the guardrail ran, containing'content': 'My SSN is 123-45-6789 and my email is jane.doe@example.com...'verbatim.After (ed8957e)
Captured at
6b7009391f; the only delta since (ed8957e5b2) excludeslitellm_logging_objfrom the persisted snapshot and adds tests for that, neither of which touches themessages/email field measured below.SpendLogs audit trail only holds the masked token
"OK"(prompt_tokens=33, same masked call as before).docker exec pg psql -U litellm -d litellm -tAc 'SELECT proxy_server_request FROM "LiteLLM_SpendLogs" LIMIT 1;' | grep -o "jane.doe@example.com\|<EMAIL_ADDRESS>"-><EMAIL_ADDRESS>(the raw email no longer appears anywhere in the output)--detailed_debuglog only shows the masked token--detailed_debugon.grep -n "receiving data" proxy.log-> one line, now fromcommon_request_processing.py:1870(after the guardrail runs), containing'content': 'My SSN is 123-45-6789 and my email is <EMAIL_ADDRESS>...'. Only one "receiving data" line is printed now (the pre-guardrail dump was removed), and it never carries the raw email.Type
🐛 Bug Fix
Caveats
logging_onlymode before reaching external loggers) is fixed and covered by a regression test that fails pre-fix, but I could not produce a clean live before/after for it: this codebase has a separate, always-onUnifiedLLMGuardrailsresponse-scanning path that already re-masks the response for this exact test scenario regardless of my change, so both legs showed masked output live. The unit-level fix and its mutation-verified test still stand on their own.Final Attestation