fix(authz): commit recorded status before flushing in ResponseFilteri… - #6335
Open
Yanhaoxi wants to merge 1 commit into
Open
fix(authz): commit recorded status before flushing in ResponseFilteri…#6335Yanhaoxi wants to merge 1 commit into
Yanhaoxi wants to merge 1 commit into
Conversation
…ngWriter ResponseFilteringWriter is write-behind: Write buffers the body and WriteHeader only records the status, with the real write and filter happening in FlushAndFilter. Its non-2xx passthrough branch is safe only because a fetch-based MCP client gates list delivery on response.ok (200-299), so a non-2xx list body is never consumed. In the production transparent-proxy path (httputil.ReverseProxy with FlushInterval: -1), the proxy calls Flush() while copying the backend response. The first Flush() on a fresh net/http writer commits the headers with an implicit WriteHeader(200) before FlushAndFilter() runs. FlushAndFilter then takes the non-2xx passthrough branch from the recorded status (e.g. 500), its WriteHeader(500) is a no-op, and the unfiltered buffered list body is delivered under a fabricated 200 -- the stacklok#5257-class bypass on the non-2xx branch. Legitimate 4xx/5xx statuses are silently rewritten to 200 as well. Commit the recorded status (rfw.statusCode) in Flush() before flushing downstream, so the wire status reflects the real backend status. SSE (statusCode 200) is unaffected and later WriteHeader calls in FlushAndFilter become no-ops instead of corrupting the status. Adds a regression test over the production wiring (real HTTP server + ReverseProxy FlushInterval:-1) asserting a 500/404 list response reaches the client with the non-2xx status while 2xx responses are still filtered. The test fails without the fix (status rewritten to 200).
Yanhaoxi
requested review from
ChrisJBurns,
JAORMX,
jhrozek,
rdimitrov and
tgrunnagle
as code owners
August 14, 2026 16:56
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #6335 +/- ##
==========================================
- Coverage 72.96% 72.96% -0.01%
==========================================
Files 742 742
Lines 78236 78237 +1
==========================================
- Hits 57088 57084 -4
- Misses 17168 17177 +9
+ Partials 3980 3976 -4 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ResponseFilteringWriterfilters 2xx list responses (tools/list,prompts/list,resources/list,find_tool) against Cedar policies. Its non-2xx passthrough branch (FlushAndFilter) is safe only because a fetch-based MCP client gates list delivery onresponse.ok(200-299). In the production transparent-proxy path (httputil.ReverseProxywithFlushInterval: -1,pkg/transport/proxy/transparent/transparent_proxy.go:1220-1221), the proxy callsFlush()while copying the backend response, and the firstFlush()on a fresh net/http writer commits the headers with an implicitWriteHeader(200)beforeFlushAndFilter()runs.FlushAndFilterthen takes the non-2xx passthrough branch from the recorded status (e.g. 500), itsWriteHeader(500)is a no-op, and the unfiltered buffered list body is delivered under a fabricated 200 — the #5257-class bypass on the non-2xx branch. Legitimate 4xx/5xx statuses are also silently rewritten to 200 (functional breakage).WHAT changed:
ResponseFilteringWriter.Flush()now commits the recorded status (rfw.ResponseWriter.WriteHeader(rfw.statusCode)) before flushing the downstream writer, so the wire status reflects the real backend status instead of the implicit 200. SSE (statusCode 200) is unaffected; laterWriteHeadercalls inFlushAndFilterbecome no-ops instead of corrupting the status.ReverseProxyFlushInterval:-1+ResponseFilteringWriter) asserting a 500/404 list response reaches the client with the non-2xx status while 2xx responses are still filtered.Fixes #6332
Type of change
Test plan
task test)task test-e2e)task lint-fix)Run:
go test ./pkg/authz/.... New test:TestResponseFilteringWriter_Non2xxStatusPreservedOnWire(inpkg/authz/response_filter_status_commit_test.go). Verified the new test FAILS without the source fix (backend 500/404 rewritten to 200 on the wire) and PASSES with it; fullpkg/authz/...suite passes.Lint:
golangci-lint run ./pkg/authz/reports only 3 pre-existinggciissues in files this PR does not touch (pkg/authz/annotation_cache.go,pkg/authz/authorizers.go,pkg/authz/config.go); neither of this PR's files is flagged.go vetclean. (gofmton a Windows checkout withcore.autocrlf=trueflags every Go file in the repo due to CRLF working-tree line endings; commits are LF-normalized and CI runs on Linux.)API Compatibility
v1beta1API.Changes
pkg/authz/response_filter.goFlush()commits the recorded status (WriteHeader(rfw.statusCode)) before flushing downstreampkg/authz/response_filter_status_commit_test.goDoes this introduce a user-facing change?
Yes: a backend answering a list method with a non-2xx status (e.g. 500) can no longer have the full, unfiltered tools/prompts/resources list delivered to the client as HTTP 200; the client now observes the real non-2xx status. Legitimate 4xx/5xx backend statuses are no longer silently rewritten to 200.
Implementation plan
Approved implementation plan
One root cause: the deferred-write design assumes
FlushAndFiltercontrols header-commit timing, but the streaming-supportFlush()touches the downstream writer first and commits an implicit 200.Flush()(response_filter.go): after deletingContent-Length, commit the recorded status beforeflusher.Flush(). Idempotent for repeated flushes (headers already committed → no-op).ReverseProxyFlushInterval:-1chain and asserts 500/404 reach the client unchanged while a 200 list is still filtered.Special notes for reviewers
Flush()plus a comment explaining why. SSE (statusCode 200) and the 2xx filtering path are unaffected — committing 200 is identical to the previous implicit 200.AuthorizationMiddleware; the exercised behavior (deferred write + early streaming flush + filter) is identical.