Skip to content

Stop SSE filter from leaking tools/list on undecodable lines#5304

Open
saivedant169 wants to merge 1 commit into
stacklok:mainfrom
saivedant169:fix/authz-sse-line-fallthrough
Open

Stop SSE filter from leaking tools/list on undecodable lines#5304
saivedant169 wants to merge 1 commit into
stacklok:mainfrom
saivedant169:fix/authz-sse-line-fallthrough

Conversation

@saivedant169
Copy link
Copy Markdown

Summary

ResponseFilteringWriter.processSSEResponse used to abort filtering of the entire SSE stream whenever a single data: line failed jsonrpc2.DecodeMessage or decoded to a non-*jsonrpc2.Response message. The fallback wrote the entire buffered upstream payload and returned, so any subsequent data: line containing the real tools/list reply reached the client unfiltered, bypassing the cedar authorization filter. The fallback also re-called WriteHeader after the reverse proxy's first Flush() had already emitted headers, producing the http: superfluous response.WriteHeader call from … response_filter.go:191 warning that surfaced the bug.

This PR:

  • Treats an undecodable or non-Response data: line as pass-through for that line only: fall through to the existing line writer (the loop's if !written branch) rather than dumping rawResponse and returning.
  • Drops the explicit WriteHeader(rfw.statusCode) calls in the fallback branches, which removes the double-header warning at line 191.
  • Emits a WARN log when a tools/list reply contains a data: line that bypasses the filter, so future bypasses are visible in audit logs instead of silent.

Notifications interleaved on a response stream are explicitly allowed by the MCP spec, so this case can be hit by fully spec-compliant upstreams (notifications/message, progress updates, etc.) as well as by upstreams fronted by an SSE bridge (the npm mcp-proxy case from the issue reproduction).

Closes #5257

Type of change

  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation
  • Other (describe):

Test plan

  • Unit tests (task test)
  • Linting (task lint-fix)
  • Manual testing (describe below)

Added TestResponseFilteringWriter_SSE_PerLineFallthrough as a table-driven regression test covering both branches (decode error and non-Response decode). It builds an SSE stream that interleaves a notification (or an undecodable garbage line) with a real tools/list response, then asserts:

  1. The preceding line is preserved verbatim on the wire.
  2. The tools/list response is filtered to only authorized tools.
  3. The literal string "admin_tool" (the unauthorized tool from the unfiltered upstream payload) does not appear anywhere in the output.

The test fails on the prior code with admin_tool reaching the recorder, and passes on the patched code.

Verified on pkg/authz/... with -race:

  • go test -ldflags=-extldflags=-Wl,-w -race ./pkg/authz/... passes (including the existing JSON-path tests and the new SSE test).
  • golangci-lint run ./pkg/authz/... reports 0 issues.

Does this introduce a user-facing change?

Yes. On SSE upstreams (Content-Type: text/event-stream), tools/list, prompts/list, and resources/list responses now respect the configured cedar authorization filter even when the upstream interleaves notifications or sends an undecodable data: line. Previously such streams returned the unfiltered tool/prompt/resource catalog to the caller. Filtered behavior is what operators already get on application/json upstreams; this brings SSE in line.

Special notes for reviewers

The change is intentionally minimal. I considered also auditing processJSONResponse for the same shape (return rawResponse on per-line failure) but it processes a single message, not a stream, so the same code shape there is already correct. Happy to extend if you want a defensive log added on that side too.

processSSEResponse used to write the entire raw upstream payload and
return whenever a single SSE data line failed jsonrpc2.DecodeMessage or
decoded to a non-Response message (e.g. a notifications/* frame). On a
tools/list reply that meant every subsequent data line, including the
real Response, reached the client unfiltered, silently bypassing the
cedar authorization filter and producing the superfluous WriteHeader
warning at response_filter.go:191.

Treat undecodable or non-Response data lines as pass-through for that
line only: fall through to the existing line writer instead of dumping
rawResponse. The explicit WriteHeader calls go away with them, which
also removes the double-header warning that surfaced the bug. Skipped
filtering on tools/list now emits a WARN so future bypasses are
visible in audit logs.

Adds a table-driven regression test covering both branches (decode
error and non-Response). It fails on the old code with the unfiltered
admin_tool entry reaching the recorder.

Closes stacklok#5257
@jhrozek
Copy link
Copy Markdown
Contributor

jhrozek commented May 18, 2026

This PR looks like it's targetting issue #5292 ?

@jhrozek
Copy link
Copy Markdown
Contributor

jhrozek commented May 18, 2026

cc @amirejaz

@amirejaz
Copy link
Copy Markdown
Contributor

Not quite — #5304 closes #5257, which is an authorization bypass in the Cedar SSE response filter (pkg/authz/response_filter.go): when a data: line fails to decode as a *jsonrpc2.Response (e.g. an interleaved notification), the filter was dumping the entire raw unfiltered upstream payload, bypassing Cedar entirely.

Our issue #5292 is a separate concern in a different layer: per-event JSON-RPC frame validation in the transparent proxy (pkg/transport/proxy/transparent/), analogous to what #5288 did for streamable-HTTP. #5292 is about the proxy rejecting/closing-stream on structurally malformed frames, not about the Cedar filter.

The two are complementary — #5304 fixes a more serious auth bypass, #5292 is about frame validation in the non-auth transparent proxy path.

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.

authz: SSE response filter leaks unfiltered tools/list on undecodable / non-Response data lines (bypasses cedar)

3 participants