Repository navigation
chore(release): backport #41880, #43956, #44011 to stable/1.103.x - #44377
PhimmStraiker wants to merge 3 commits into
Conversation
…/v3/detect) (BerriAI#41880) (cherry picked from commit e0af991) The guardrail test's gateway name is neutral (Acme Gateway), as in BerriAI#43576.
…ad of skipping the guardrail (BerriAI#43956) (cherry picked from commit a3a7650) The pick drops its hunk in tests/integration/observability/test_straiker_v3_platform.py, a file stable/1.103.x does not have.
…ed on a missing verdict (BerriAI#44011) (cherry picked from commit 826b21a)
|
|
|
| supplied: Final = _request_header(request_data, V3_SESSION_HEADER) | ||
| if supplied: | ||
| return supplied |
There was a problem hiding this comment.
Sessions Can Cross Users If two authenticated users send the same
x-claude-code-session-id and opening turn, this code sends the same raw session ID to Straiker for both. Unlike the derived session ID, it does not separate users. Straiker may treat the second turn as an already-scored replay and allow it without scoring it; the local block cache does not prevent this because it is scoped per user.
How this was verified: The inbound session header takes precedence over the user-separated ID and is sent to Straiker, which skips previously scored turns within a session.
There was a problem hiding this comment.
Verbatim pick of #41880; main sends the same session header. Any change belongs on main first, then picked here.
There was a problem hiding this comment.
That’s fair. Since this is a verbatim backport of #41880 and the session-header behavior is unchanged from main, this PR should not diverge from its source commit. I’m withdrawing this finding for the backport; any cross-user session isolation fix belongs in #41880/main first and can then be backported here.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| agent: Final = ( | ||
| agent_ref or _request_header(request_data, V3_AGENT_HEADER) or (recognised[1] if recognised else None) | ||
| ) |
There was a problem hiding this comment.
Caller Can Select Agent When
agent_ref is unset, a caller can supply x-s6r-agent with another application's agent name. This code forwards that name to Straiker without restricting it, so the caller's traffic can be checked under the other application's controls instead of those intended for its route.
How this was verified: The inbound agent header is accepted when agent_ref is unset and forwarded as Straiker's agent routing hint.
There was a problem hiding this comment.
Understood. The intended contract is that agent_ref pins the route when configured, while an unset agent_ref permits the caller-supplied x-s6r-agent; that matches this implementation and the verbatim backport. Given the prior withdrawal on #43943, I’m withdrawing this finding.
| the hook runs. Straiker's coding-agent reader parses a Messages answer, so a Claude Code | ||
| turn sent as a chat completion scores nothing; the proxy's own adapter turns it back. | ||
| """ | ||
| response: Final = request_data.get("response") |
There was a problem hiding this comment.
High: Client-controlled response bypasses output screening
An authenticated caller can include a benign native response object in a /v1/messages request: ingress leaves it intact, the provider parameter filter drops it from the upstream request, and AnthropicMessagesHandler._prepare_request_data preserves it instead of storing the real answer. This new path then screens that decoy rather than the actual output texts/tool calls, allowing the real answer past post-call controls; strip client-supplied response at ingress and have translation handlers overwrite it with trusted provider output.
There was a problem hiding this comment.
Confirmed, it reproduces on main too: a client-supplied response survives into request_data. We will fix it on main first, then pick it here.
| is_unreachable=False, | ||
| ) | ||
|
|
||
| replayed: Final = self._v3_replayed_block(scope, prefixes) if input_type == "request" else None |
There was a problem hiding this comment.
High: Replay bypasses block enforcement
With Straiker's replay-allow behavior documented in this PR, an authenticated caller can submit two identical turns concurrently and get one forwarded despite the other's block: both miss this cache before awaiting detection, and the replay's allow is accepted without checking the original verdict. Retrying on another worker also bypasses the per-instance cache; preserve original verdicts for replays and coordinate scoring atomically across workers rather than treating a replay allow as a fresh authorization.
There was a problem hiding this comment.
Verbatim pick; main has the same per-process cache. Re-scoring replays is a Straiker platform fix, tracked on our side.
PR overviewThis PR backports changes from #41880, #43956, and #44011 to stable/1.103.x, including updates to Straiker guardrail replay handling and response screening for Anthropic Two issues remain open, with none addressed so far. Authenticated callers can bypass block enforcement through concurrent or cross-worker replays, and can supply a decoy response that is screened instead of the actual provider output. These paths undermine both request blocking and post-call controls, allowing content that should be blocked to pass through. Open issues (2)
Fixed/addressed: 0 · PR risk: 7/10 |
TLDR
Problem this solves:
sk_agt_...) get a 401 on stable/1.103.x/api/v3/detectHow it solves it:
api_versionno longer skips the guardrailUser Flow
Before: a proxy admin on 1.103 rotates the Straiker guardrail to a v3 key and every guarded request fails
api_keyto ansk_agt_...key and restarts the proxyauthentication_errorAfter: the same key works and a block reaches the developer
sk_agt_...key and restarts the proxyx-litellm-applied-guardrails: Straiker,Straiker-postAdmins on existing v1 keys see no change
Relevant issues
Backports #41880, #43956 and #44011 onto
stable/1.103.x, the same way #43943 carried #41880 to 1.101.4#41941, which #41880 needs so an unset
api_versionstays unset, is already on this line. #43329 (theexperimental_pass_throughtopass_throughrename) is not included: this line still hasexperimental_pass_through, and the picks keep that import. #42781 and #44153 add integration tests in a file this line does not have, so neither is included, and #43956's hunk in that file is dropped from its pickThe picks are otherwise verbatim: the patch-ids of the
litellm/changes match the commits on main, and the test hunks are identical apart from the test file's path (tests/test_litellm/...on this line) and one test fixture, whose gateway name is neutral (Acme Gateway) as in #43576. The version bump and release cut are left to the maintainersPre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
uv run pytest tests/unit/<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)Screenshots / Proof of Fix
Shared setup: a real proxy on
127.0.0.1built from this branch, servinggpt-4o-mini(real OpenAI calls) andclaude-haiku-4-5-20251001(real Anthropic calls), with a pre_call and a post_call Straiker guardrail pointed at a real Straiker v3 tenant athttps://api.prod.straiker.aiwith ansk_agt_key and noapi_versionset. No database.$LITELLM_MASTER_KEYis the proxy master key. Outputs are trimmed to the status line, the guardrail header and the start of the bodyBefore (b74a249)
Unnamed app
https://api.prod.straiker.ai/api/v1/detect/webhookNamed agent, prompt injection
Claude Code, streamed /v1/messages
After (6c9e5a0)
Unnamed app
https://api.prod.straiker.ai/api/v3/detect, with nostraiker.errorevents and no tracebacksNamed agent, prompt injection
action: block,blocked_by: ["llm_evasion"]. Straiker flagged this phrasing on 2 of 4 sends in this run; the other two came backallowfrom Straiker and were served, which is the tenant's detection, not the relayClaude Code, streamed /v1/messages
message_stop, first text delta"p"/api/v3/detect, which exercises theexperimental_pass_throughanswer translation this line needsTests
tests/test_litellm/proxy/guardrails/guardrail_hooks/test_straiker.py,test_javelin.pyand theazure/guardrail tests: 196 passed on this branchType
🆕 New Feature
🐛 Bug Fix
Caveats (if any)
Low