[Bug]: Streaming guardrails: value split across two SSE chunks can pass per-chunk checks
Check for existing issues
- I have searched the existing issues and checked that my issue is not a duplicate.
What happened?
When streaming a chat completion through the proxy with guardrails enabled, sensitive values that are split across consecutive Server-Sent Event (SSE) chunks can pass inspection unmasked because each chunk is evaluated in isolation.
Expected: An inquiry into whether guardrail evaluation can buffer or retain state across chunk boundaries so fragmented entities are detected, or if per-chunk inspection is the intended design for streaming throughput.
User Flow
Before a (hypothetical) fix: The flow fails when a protected string is split across consecutive streaming chunks and arrives unmasked at the client.
- The client sends a POST request to
http://127.0.0.1:4000/v1/chat/completionswith"stream": truerequesting text that contains a sensitive value (for example, an email address). - The proxy responds with HTTP 200 and
Content-Type: text/event-stream. - The client receives Chunk 1:
data: {"choices":[{"delta":{"content":"user@exa"}}]}. - The client receives Chunk 2:
data: {"choices":[{"delta":{"content":"mple.com"}}]}. - The client concatenates the chunks in the browser or terminal, reassembling
"[email protected]". Security consequence: The recipient observes the unmasked sensitive string because neither isolated chunk matched the redaction pattern on the wire.
After a (hypothetical) fix: The flow succeeds because boundary tokens are retained across chunks so the reassembled entity is masked before reaching the client.
- The client sends a POST request to
http://127.0.0.1:4000/v1/chat/completionswith"stream": truerequesting text that contains a sensitive value (for example, an email address). - The proxy responds with HTTP 200 and
Content-Type: text/event-stream. - The client receives Chunk 1: the proxy buffers trailing characters or yields only non-sensitive tokens.
- The client receives Chunk 2: the proxy evaluates the cross-chunk boundary and replaces the reassembled entity with a redaction marker (e.g.
[EMAIL]). - The client concatenates the chunks in the browser or terminal, receiving
[EMAIL]. Security consequence: The recipient can no longer observe the unmasked sensitive string in the stream.
Proof the bug occurs
Context / Framing
This is an architectural inquiry and edge-case test regarding cross-chunk state accumulation in streaming guardrail hooks, rather than a crash report.
The Mechanism
When an upstream provider streams tokens that fragment a sensitive value across two consecutive Server-Sent Events (SSE), per-chunk guardrails can miss the pattern:
- Event 1:
data: {"choices":[{"delta":{"content":"user@exa"}}]} - Event 2:
data: {"choices":[{"delta":{"content":"mple.com"}}]}
Neither individual chunk triggers an email pattern match, but the client reassembles "[email protected]".
Configuration
model_list:
- model_name: gpt-4o-mini
litellm_params:
model: gpt-4o-mini
litellm_settings:
callbacks: ["presidio"]
Reproduction Harness
Because reproducing token fragmentation deterministically against live LLM providers is expensive and non-deterministic, I built a zero-credential, endpoint-neutral CI harness (pii-leak-benchmark) that injects deterministic split-chunk boundaries directly into the streaming proxy:
- uses: ninadphalak/[email protected]
with:
target-base-url: http://127.0.0.1:4000/v1
start-command: ./scripts/start-litellm.sh
upstream-env: OPENAI_API_BASE
Questions for Maintainers
- Does LiteLLM's streaming guardrail hook currently retain a sliding window / accumulated text across chunks, or evaluate each chunk independently?
- Would an automated CI regression check for split-chunk evasion be useful to the project? Happy to submit a PR.
Harness reference: https://pypi.org/project/pii-leak-benchmark/
What part of LiteLLM is this about?
Proxy
What LiteLLM version are you on ?
1.99.0
Twitter / LinkedIn details
Source: BerriAI/litellm