Skip to content

fix(guardrails): register sensitive_data_routing as a built-in guardrail - #37480

Open
pjdurden wants to merge 4 commits into
BerriAI:mainfrom
pjdurden:fix/36535
Open

pjdurden wants to merge 4 commits into
BerriAI:mainfrom
pjdurden:fix/36535

Conversation

@pjdurden

Copy link
Copy Markdown

Automated draft from oss-work.

Copilot AI lite review requested due to automatic review settings August 19, 2026 15:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR registers sensitive_data_routing as a built-in pre-call guardrail and adds detector-based rerouting with configurable sticky-session lifetime

  • Adds guardrail discovery, configuration types, pattern matching, and regression tests
  • Propagates guardrail-specific session TTLs into tenant-scoped routing caches

Confidence Score: 3/5

The non-positive session TTL path must be rejected before merge because it can disable persistence of sensitive-session routing

The new configuration accepts zero and negative TTLs, then forwards them unchanged to cache backends that immediately expire or reject the routing override

Files Needing Attention: litellm/types/proxy/guardrails/guardrail_hooks/sensitive_data_routing.py and litellm/proxy/hooks/sensitive_data_routing.py

Security Review

The new TTL configuration accepts non-positive values, which can prevent sensitive-session routing overrides from persisting

Important Files Changed

Filename Overview
litellm/proxy/guardrails/guardrail_hooks/sensitive_data_routing/init.py Registers and initializes the new guardrail through the repository's automatic package discovery mechanism
litellm/proxy/guardrails/guardrail_hooks/sensitive_data_routing/sensitive_data_routing.py Implements pre-call pattern detection and raises a model-routing exception carrying sticky-session configuration
litellm/types/proxy/guardrails/guardrail_hooks/sensitive_data_routing.py Defines the guardrail configuration but fails to constrain session_ttl_seconds to positive values
litellm/proxy/hooks/sensitive_data_routing.py Applies per-guardrail TTL overrides directly to Redis and local routing caches
litellm/proxy/utils.py Propagates the guardrail-specific TTL when handling sensitive-data routing exceptions
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_sensitive_data_routing.py Covers registration, initialization, detector matching, rerouting, and sticky-session flags, but not invalid TTL values

Reviews (1): Last reviewed commit: "fix: BerriAI/litellm#36535 - [Bug]: sens..." | Re-trigger Greptile

Comment on lines +35 to +38
session_ttl_seconds: int = Field(
default=DEFAULT_SESSION_TTL_SECONDS,
description="How long a session stays pinned to the on-premise model after detection.",
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Non-positive TTL breaks session pinning

When session_ttl_seconds is non-positive, cache entries expire immediately or Redis rejects them, leaving later session requests unpinned.

How this was verified: The configured value flows unchanged into both cache TTL arguments.

Suggested change
session_ttl_seconds: int = Field(
default=DEFAULT_SESSION_TTL_SECONDS,
description="How long a session stays pinned to the on-premise model after detection.",
)
session_ttl_seconds: int = Field(
default=DEFAULT_SESSION_TTL_SECONDS,
ge=1,
description="How long a session stays pinned to the on-premise model after detection.",
)

Knowledge Base Used: Guardrails


session_id: Final = get_session_id_from_request_data(request_data)
raise SensitiveDataRouteException(
route_to_model=self.on_premise_model,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Rerouted model bypasses model-access controls

Model authorization runs against the caller-supplied model before pre-call guardrails execute, while the exception handler replaces it with on_premise_model without repeating those checks. A caller allowed only the original model can include a configured keyword or sensitive-data pattern and invoke the restricted target model; authorize the resolved target against the key, team, user, project, and member scopes before applying either the immediate or sticky routing override.

for name in prebuilt_patterns or ()
),
*(
SensitiveDataDetector("regex_pattern", pattern, re.compile(pattern, re.IGNORECASE))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Low: Custom regex permits request-driven denial of service

These operator-supplied patterns are evaluated with Python's backtracking regex engine synchronously in the async request path. If a supported configuration contains a nested-quantifier pattern such as (a+)+$, a caller can submit a crafted prompt that monopolizes the worker; reject unsafe expressions or evaluate custom patterns with a bounded matching engine and timeout.

@veria-ai

veria-ai Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

PR overview

The PR registers sensitive_data_routing as a built-in guardrail that routes requests to an on-premise model when configured keywords or sensitive-data patterns are detected.

One issue has been addressed, but three remain open. Sensitive-data routing can invoke a target model outside the caller’s authorized scope, and caller-supplied cloud fallbacks can send detected sensitive content off-premise when the on-premise model’s context limit is exceeded. Custom regex matching also allows a crafted request to monopolize a worker if an operator configures a vulnerable pattern.

Open issues (3)

Fixed/addressed: 1 · PR risk: 7/10

@codecov

codecov Bot commented Aug 19, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.76271% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...guardrail_hooks/sensitive_data_routing/__init__.py 88.00% 3 Missing ⚠️
...s/sensitive_data_routing/sensitive_data_routing.py 97.33% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

⚠️ 1 benchmark spent significant time in system calls

System calls cannot be consistently instrumented, so they are not included in the measure, which understates the real cost. Please switch to the Walltime instrument to accurately measure system calls.

Measurement and system calls

✅ 31 untouched benchmarks


Comparing pjdurden:fix/36535 (d4c3062) with main (19327a6)

Open in CodSpeed

@pjdurden

Copy link
Copy Markdown
Author

went through the three findings, took one and am pushing back on the other two.

non-positive TTL (P1) — real, and mine. session_ttl_seconds is a field this PR adds, and it flows straight into both cache TTL args, so 0 expires the in-memory pin immediately and Redis rejects the SETEX (swallowed by the surrounding try/except). the pin silently never sticks, which is the opposite of what the operator asked for. taken as suggested, ge=1 on the field, plus a parametrized regression test for 0 and -1. confirmed both cases fail without the bound and pass with it. 15 passed in that file now, 59 in the hook file.

rerouted model bypasses model-access controls — the concern is fair but it isnt this PR. the two places the model gets swapped, _handle_sensitive_data_route_exception in proxy/utils.py and async_pre_call_hook in hooks/sensitive_data_routing.py, both predate this branch. git log -L on those lines lands on the merge base, not on my commit. my diff to either file is the one ttl= argument. the reroute path was already reachable by any guardrail configured with on_sensitive_data='route'; this PR adds another producer of the same exception, it doesnt open the path.

theres also a design question underneath it. on_premise_model is operator-configured and the whole point is to divert sensitive prompts there, so it usually wont be in the caller's allowed list. authorizing the resolved target against key/team/user scope would break the feature for the config in the docs. if maintainers want that check it should be a separate change against the pre-existing handler, with a decision on what happens when the target isnt in scope, deny or fall back. happy to open an issue for it, just not going to change unrelated auth behaviour inside a registration fix.

custom regex ReDoS — patterns come from regex_patterns in the proxy config, so the input is the operator, not the caller. an operator who writes (a+)+$ can already do the same through the existing content-filter guardrail, which compiles config-supplied patterns the same way. bounding this properly means a different engine or a timeout across every regex guardrail in the repo, which is worth doing but not inside this PR.

@pjdurden pjdurden changed the title fix: BerriAI/litellm#36535 - [Bug]: sensitive_data_routing documented but not recognized as guardrail fix(guardrails): register sensitive_data_routing as a built-in guardrail Aug 21, 2026
@yuneng-berri
yuneng-berri deleted the branch BerriAI:main September 13, 2026 04:43
@yuneng-berri yuneng-berri reopened this Sep 13, 2026
@pjdurden

Copy link
Copy Markdown
Author

Status check on this one, since it has been quiet since Aug 21.

The only red check is the Vertex AI job, and it is not from this PR. The four failures are all embedding-cost assertions in gemini_embeddings/test_batch_embed_content_transformation.py:

test_file_reference_image_billed_per_image_not_text  assert 0.0 == 0.00012
test_file_reference_non_image_not_counted_as_image   assert 0.000416 == 0.00032
test_multimodal_image_preserves_usage_metadata       assert 0.0 > 0
test_video_plus_audio_does_not_double_bill_text      assert 0.0004162 == 0.0019002
4 failed, 1556 passed

Byte-identical to the four failing on my other open PR #37291, which shares no code with this one, so this looks like base-branch pricing drift rather than anything either branch did. This PR touches the guardrail registration, litellm/exceptions.py and litellm/proxy/utils.py, nothing under llms/vertex_ai/.

The three review findings from Aug were resolved in the thread above: I took the non-positive TTL one, since session_ttl_seconds is a field this PR adds and 0 would expire the in-memory pin immediately while Redis rejects the SETEX, and I pushed back on the other two with the reasoning inline.

So this is waiting on a first human review rather than on anything from my side. @krrishdholakia @ishaan-jaff when one of you has a moment.

@CLAassistant

CLAassistant commented Oct 7, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@pjdurden

pjdurden commented Oct 7, 2026

Copy link
Copy Markdown
Author

merged current main in (52271a3d43). same get_model_group_info conflict as #37291 (kept main's get_model_list_of_routed_group, description computed off it), plus the two new session ttl tests landed next to a test main reformatted, kept both. guardrail_hooks and hooks sensitive_data_routing tests pass (76).

… recognized as guardrail

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
A non-positive TTL either expires the in-memory pin immediately or makes
Redis reject the SETEX, so sticky_session silently never pinned. Bound the
field at config-parse time instead of failing quietly at request time.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
The config-model pattern lists become Sequence[str], which the guardrail
already accepted and which generates an identical JSON schema. The remaining
four are fixed by CustomGuardrail's own signatures, so they carry a
mutable-ok reason instead. Comments are kept short enough that ruff format
leaves them on the annotation line, which is where the gate reads them.

Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
Assistant tool_calls reach the guardrail through inputs["tool_calls"],
not texts, so an SSN inside function.arguments was forwarded to the
cloud model without triggering a reroute. Scan the raw argument string
and its decoded JSON string values alongside the message text.

Also replaces the two-for generator in detect() with a plain loop and
declares SensitiveDataDetectorKind as a TypeAlias, for LIT014/LIT010.
return inputs

session_id: Final = get_session_id_from_request_data(request_data)
raise SensitiveDataRouteException(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: Cloud fallbacks bypass sensitive-data routing

When configured globally with default_on: true, a caller can submit sensitive text with fallbacks: ["cloud-model"] and exceed the on-premise model's smaller context window: the exception handler changes only model, so the router forwards the unchanged prompt to the cloud fallback without rerunning the global guardrail. Disable fallbacks after sensitive-data detection, or restrict every fallback to approved on-premise targets, including requests restored from sticky-session pins.

This branch has not been deployed

No deployments
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.

4 participants