Skip to content

fix(guardrails): pii/pci guardrail masking gaps in SpendLogs, debug logs, and logging_only response - #37965

Merged
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_pii_guardrail_storage_leak
Aug 22, 2026
Merged

fix(guardrails): pii/pci guardrail masking gaps in SpendLogs, debug logs, and logging_only response#37965
yassin-berriai merged 1 commit into
litellm_internal_stagingfrom
litellm_pii_guardrail_storage_leak

Conversation

@yassin-berriai

@yassin-berriai yassin-berriai commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • PII/PCI guardrail masking doesn't reach everything the proxy stores or logs
  • Raw PII still lands in SpendLogs and debug logs after masking

How it solves it:

  • Re-snapshot the SpendLogs audit body after guardrails mask the request
  • Move the raw-request debug log to after masking runs, not before
  • Mask the model's response too before external log callbacks see 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

  1. Admin configures the pii-masking guardrail and sets store_prompts_in_spend_logs: true
  2. A user sends POST https://litellm-domain/v1/chat/completions with "My SSN is 123-45-6789 and my email is jane.doe@example.com"
  3. The model only ever sees the masked prompt and replies normally
  4. The admin queries the request's stored audit record and finds the raw, unmasked email sitting there anyway
  5. With --detailed_debug on, the proxy's own debug log also prints the raw email

After: the same setup, but nothing raw ever reaches storage or logs

  1. Admin configures the pii-masking guardrail and sets store_prompts_in_spend_logs: true
  2. A user sends the same POST https://litellm-domain/v1/chat/completions request with the same PII
  3. The model only ever sees the masked prompt and replies normally
  4. The admin queries the request's stored audit record and finds only the masked <EMAIL_ADDRESS> token
  5. The debug log output also only ever shows the masked token, never the raw email

Relevant issues

Linear ticket

Resolves LIT-6015

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. 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
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to 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-anonymizer containers, general_settings.store_prompts_in_spend_logs: true, and a pii-masking Presidio guardrail (mode: pre_call, default_on: true) pointed at them. Every request below hit the real OpenAI API and cost real money.

curl -s http://localhost:26018/v1/chat/completions \
  -H "Authorization: Bearer sk-lit6015-test" -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"My SSN is 123-45-6789 and my email is jane.doe@example.com. Reply with just the word OK."}]}'

Before (7a1afa1)

SpendLogs audit trail still holds the raw email

  1. Sent the request above. Model replied "OK" (prompt_tokens=33, confirming Presidio really masked the outbound call).
  2. 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.com
  3. The stored audit record's body.messages still contains the raw email, even though the model itself never received it.

--detailed_debug log leaks the raw email

  1. Same request as above, proxy started with --detailed_debug.
  2. grep -n "receiving data" proxy.log -> one line from litellm_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) excludes litellm_logging_obj from the persisted snapshot and adds tests for that, neither of which touches the messages/email field measured below.

SpendLogs audit trail only holds the masked token

  1. Sent the identical request. Model replied "OK" (prompt_tokens=33, same masked call as before).
  2. 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_debug log only shows the masked token

  1. Same request, --detailed_debug on.
  2. grep -n "receiving data" proxy.log -> one line, now from common_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

  • A third gap (response never masked in logging_only mode 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-on UnifiedLLMGuardrails response-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

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The current head completes the audit-snapshot fix by excluding the live logging object while retaining the post-guardrail request state.

  • Refreshes the persisted request snapshot after pre-call guardrails run.
  • Moves detailed request logging after masking.
  • Applies masking to supported non-streaming responses before external logging callbacks.
  • Adds regression coverage for masked snapshots, response masking, logging-object exclusion, and JSON serializability.

Confidence Score: 5/5

The 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.

Important Files Changed

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

Comment thread litellm/proxy/common_request_processing.py
@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...tellm/proxy/guardrails/guardrail_hooks/presidio.py 75.00% 1 Missing ⚠️

📢 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
@yassin-berriai
yassin-berriai force-pushed the litellm_pii_guardrail_storage_leak branch from 6b70093 to ed8957e Compare August 22, 2026 19:57
@yassin-berriai

Copy link
Copy Markdown
Contributor Author

@greptileai the runtime logging-object leak is fixed: refresh_proxy_server_request_body_snapshot now also excludes litellm_logging_obj, pinned by two tests asserting it's absent and the persisted body is JSON-serializable. Please re-review the current head ed8957e.

@yassin-berriai yassin-berriai changed the title fix(guardrails): PII/PCI guardrail masking gaps in SpendLogs, debug logs, and logging_only response fix(guardrails): pii/pci guardrail masking gaps in SpendLogs, debug logs, and logging_only response Aug 22, 2026
@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_pii_guardrail_storage_leak (ed8957e) with litellm_internal_staging (490c9f9)

Open in CodSpeed

@yassin-berriai
yassin-berriai merged commit 9349b22 into litellm_internal_staging Aug 22, 2026
71 of 73 checks passed
@yassin-berriai
yassin-berriai deleted the litellm_pii_guardrail_storage_leak branch August 22, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants