Description
Two related issues combine to allow a client to bypass virtual server scoping.
M1: Internal header re-injection on broker passthrough path
HandleNoneToolCall (request_handlers.go:860-866) re-injects x-mcp-authorized and x-mcp-virtualserver from the original client-supplied headers after they were stripped in the headers phase (line 233). The stripping tells Envoy to remove them, but localRequestHeaders (line 104) retains the original client headers, which are copied to mcpRequest.Headers (line 242). The re-injection reads from this stale copy.
Data flow:
- Client sends
x-mcp-virtualserver: attacker-value
- Headers phase strips it (Envoy removes it)
- Body phase:
HandleNoneToolCall reads it from mcpReq.Headers (original headers still in memory) and re-injects it
- Broker receives the client-controlled value
L4: Virtual server filter fail-open
When GetVirtualSeverByHeader fails (unknown virtual server name), applyVirtualServerFilter (filtered_tools_handler.go:222-226) returns the full unfiltered tool list instead of an empty list.
Combined impact
A client injects x-mcp-virtualserver: nonexistent, the router re-injects it, the broker's lookup fails, and all tools are returned unfiltered -- bypassing virtual server scoping entirely.
Proposed Fix
- M1: Compute
x-mcp-authorized and x-mcp-virtualserver server-side from validated session state and routing config, not from client-supplied headers
- L4: Return empty tools on virtual server lookup failure (fail-closed)
Review Guidance
internal/mcp-router/request_handlers.go:860-866 -- the re-injection path
internal/broker/filtered_tools_handler.go:212-226 -- the fail-open filter
- Check all other uses of
internalOnlyHeaders for similar patterns
Manual Verification
- Deploy with virtual server configured
- Send a
tools/list request with header x-mcp-virtualserver: nonexistent
- Before fix: all tools returned. After fix: empty or error
Description
Two related issues combine to allow a client to bypass virtual server scoping.
M1: Internal header re-injection on broker passthrough path
HandleNoneToolCall(request_handlers.go:860-866) re-injectsx-mcp-authorizedandx-mcp-virtualserverfrom the original client-supplied headers after they were stripped in the headers phase (line 233). The stripping tells Envoy to remove them, butlocalRequestHeaders(line 104) retains the original client headers, which are copied tomcpRequest.Headers(line 242). The re-injection reads from this stale copy.Data flow:
x-mcp-virtualserver: attacker-valueHandleNoneToolCallreads it frommcpReq.Headers(original headers still in memory) and re-injects itL4: Virtual server filter fail-open
When
GetVirtualSeverByHeaderfails (unknown virtual server name),applyVirtualServerFilter(filtered_tools_handler.go:222-226) returns the full unfiltered tool list instead of an empty list.Combined impact
A client injects
x-mcp-virtualserver: nonexistent, the router re-injects it, the broker's lookup fails, and all tools are returned unfiltered -- bypassing virtual server scoping entirely.Proposed Fix
x-mcp-authorizedandx-mcp-virtualserverserver-side from validated session state and routing config, not from client-supplied headersReview Guidance
internal/mcp-router/request_handlers.go:860-866-- the re-injection pathinternal/broker/filtered_tools_handler.go:212-226-- the fail-open filterinternalOnlyHeadersfor similar patternsManual Verification
tools/listrequest with headerx-mcp-virtualserver: nonexistent