@@ -89,12 +89,13 @@ type HTTPProxy struct {
8989 // Keys are URL path prefixes. Set via WithPrefixHandlers.
9090 prefixHandlers map [string ]http.Handler
9191
92- // Waiters keyed by compositeKey(sessID, idKey) -> one-shot channel for response delivery.
93- // The composite key MUST be unique per concurrent request; sharing it across requests
94- // (e.g. with sessID="" for sessionless requests) silently overwrites entries and crosses
95- // response payloads between unrelated clients. See resolveSessionForRequest.
92+ // Waiters keyed by compositeKey(sessID, per-request token) -> one-shot channel for response
93+ // delivery. The composite key MUST be unique per concurrent request; sharing it across
94+ // requests silently overwrites entries and crosses response payloads between requests.
95+ // createWaiter guarantees this by minting the token itself, because neither the session
96+ // ID (empty for sessionless requests) nor the client-chosen JSON-RPC ID is unique.
9697 waiters sync.Map // map[string]chan jsonrpc2.Message
97- // Keyed by the same compositeKey(sessID, idKey) ; stores the original client JSON-RPC ID
98+ // Keyed by the same composite key ; stores the original client JSON-RPC ID
9899 // to restore before replying. Same uniqueness requirement as `waiters`.
99100 idRestore sync.Map // map[string]jsonrpc2.ID
100101
@@ -1026,10 +1027,10 @@ func (p *HTTPProxy) restoreResponseID(resp *jsonrpc2.Response, ck string) (jsonr
10261027func (p * HTTPProxy ) sendCorrelatedRequest (
10271028 sessID string , req * jsonrpc2.Request ,
10281029) (waitCh chan jsonrpc2.Message , ck string , cleanup func (), err error ) {
1029- key := idKeyFromID ( req .ID )
1030- ck = compositeKey ( sessID , key )
1031-
1032- waitCh , cleanup = p . createWaiter ( sessID , req . ID )
1030+ waitCh , ck , cleanup , err = p . createWaiter ( sessID , req .ID )
1031+ if err != nil {
1032+ return nil , "" , nil , err
1033+ }
10331034
10341035 proxiedMsg , err := encodeRequestWithID (req , ck )
10351036 if err != nil {
@@ -1099,11 +1100,12 @@ func (p *HTTPProxy) ensureSession(r *http.Request, id string) error {
10991100// in-process routing token (not registered with sessionManager). Sessionless
11001101// routing tokens MUST be unique per request: sharing one (e.g. the empty string,
11011102// or a stale/foreign Mcp-Session-Id) across concurrent sessionless requests with
1102- // the same JSON-RPC id collapses them onto the same compositeKey(sessID, idKey)
1103- // and overwrites entries in the waiters / idRestore sync.Maps, leaking one
1103+ // the same JSON-RPC id could otherwise share routing state and leak one
11041104// client's response payload to another. This is a confidentiality bug, not a
11051105// performance issue -- do not collapse the token, and do not let a Modern
1106- // request fall through to the session-lookup path below.
1106+ // request fall through to the session-lookup path below. (The waiter key
1107+ // itself is additionally made unique per request by createWaiter, so two
1108+ // requests in one session reusing an id are isolated too.)
11071109//
11081110// Writes HTTP errors on failure and returns error to stop handling.
11091111func (p * HTTPProxy ) resolveSessionForRequest (
@@ -1652,10 +1654,21 @@ func resolveRequestTimeout() time.Duration {
16521654 return defaultRequestTimeout
16531655}
16541656
1655- // createWaiter registers a waiter channel for the given request ID and returns cleanup fn.
1656- func (p * HTTPProxy ) createWaiter (sessID string , id jsonrpc2.ID ) (chan jsonrpc2.Message , func ()) {
1657- key := idKeyFromID (id )
1658- ck := compositeKey (sessID , key )
1657+ // createWaiter registers a waiter channel for a request and returns the channel,
1658+ // the composite key (ck) the request must carry upstream, and a cleanup fn.
1659+ //
1660+ // ck is minted here from a per-request random token, never from the
1661+ // client-chosen JSON-RPC id: a client may have several requests with the same id
1662+ // in flight in one session, and a key derived from the id would let them
1663+ // overwrite each other's waiter and receive each other's responses (including
1664+ // responses the authz response filter never saw). The client's original id is
1665+ // kept in idRestore and put back on the reply.
1666+ func (p * HTTPProxy ) createWaiter (sessID string , id jsonrpc2.ID ) (chan jsonrpc2.Message , string , func (), error ) {
1667+ token , err := uuid .NewRandom ()
1668+ if err != nil {
1669+ return nil , "" , nil , fmt .Errorf ("generate request correlation token: %w" , err )
1670+ }
1671+ ck := compositeKey (sessID , token .String ())
16591672 // store original client id to restore before replying
16601673 p .idRestore .Store (ck , id )
16611674
@@ -1666,5 +1679,5 @@ func (p *HTTPProxy) createWaiter(sessID string, id jsonrpc2.ID) (chan jsonrpc2.M
16661679 p .waiters .Delete (ck )
16671680 p .idRestore .Delete (ck )
16681681 }
1669- return ch , cleanup
1682+ return ch , ck , cleanup , nil
16701683}
0 commit comments