@@ -1649,11 +1649,87 @@ func TestTokenInfo(t *testing.T) {
16491649 if ! ok {
16501650 t .Fatal ("not TextContent" )
16511651 }
1652- if g , w := tc .Text , "&{[scope] 5000-01-02 03:04:05 +0000 UTC map[]}" ; g != w {
1652+ if g , w := tc .Text , "&{[scope] 5000-01-02 03:04:05 +0000 UTC map[]}" ; g != w {
16531653 t .Errorf ("got %q, want %q" , g , w )
16541654 }
16551655}
16561656
1657+ func TestSessionHijackingPrevention (t * testing.T ) {
1658+ // This test verifies that sessions bound to a user ID cannot be accessed
1659+ // by a different user (session hijacking prevention).
1660+ ctx := context .Background ()
1661+
1662+ server := NewServer (testImpl , nil )
1663+ streamHandler := NewStreamableHTTPHandler (func (req * http.Request ) * Server { return server }, nil )
1664+
1665+ // Use the bearer token directly as the user ID. This simulates how a real
1666+ // verifier might extract a user ID from a JWT "sub" claim or introspection.
1667+ verifier := func (_ context.Context , token string , _ * http.Request ) (* auth.TokenInfo , error ) {
1668+ return & auth.TokenInfo {
1669+ Scopes : []string {"scope" },
1670+ UserID : token ,
1671+ Expiration : time .Date (5000 , 1 , 2 , 3 , 4 , 5 , 0 , time .UTC ),
1672+ }, nil
1673+ }
1674+ handler := auth .RequireBearerToken (verifier , nil )(streamHandler )
1675+ httpServer := httptest .NewServer (mustNotPanic (t , handler ))
1676+ defer httpServer .Close ()
1677+
1678+ // Helper to send a JSON-RPC request as a given user.
1679+ doRequest := func (msg jsonrpc.Message , sessionID , userID string ) * http.Response {
1680+ t .Helper ()
1681+ data , _ := jsonrpc2 .EncodeMessage (msg )
1682+ req , _ := http .NewRequestWithContext (ctx , http .MethodPost , httpServer .URL , bytes .NewReader (data ))
1683+ req .Header .Set ("Content-Type" , "application/json" )
1684+ req .Header .Set ("Accept" , "application/json, text/event-stream" )
1685+ req .Header .Set ("Authorization" , "Bearer " + userID )
1686+ if sessionID != "" {
1687+ req .Header .Set ("Mcp-Session-Id" , sessionID )
1688+ }
1689+ resp , err := http .DefaultClient .Do (req )
1690+ if err != nil {
1691+ t .Fatalf ("request failed: %v" , err )
1692+ }
1693+ return resp
1694+ }
1695+
1696+ // Create a session as user1.
1697+ initReq := & jsonrpc.Request {Method : "initialize" , ID : jsonrpc2 .Int64ID (1 )}
1698+ initReq .Params , _ = json .Marshal (& InitializeParams {
1699+ ProtocolVersion : protocolVersion20250618 ,
1700+ ClientInfo : & Implementation {Name : "test" , Version : "1.0" },
1701+ })
1702+ resp := doRequest (initReq , "" , "user1" )
1703+ defer resp .Body .Close ()
1704+ if resp .StatusCode != http .StatusOK {
1705+ body , _ := io .ReadAll (resp .Body )
1706+ t .Fatalf ("initialize failed with status %d: %s" , resp .StatusCode , body )
1707+ }
1708+ sessionID := resp .Header .Get ("Mcp-Session-Id" )
1709+ if sessionID == "" {
1710+ t .Fatal ("no session ID in response" )
1711+ }
1712+
1713+ pingReq := & jsonrpc.Request {Method : "ping" , ID : jsonrpc2 .Int64ID (2 )}
1714+ pingReq .Params , _ = json .Marshal (& PingParams {})
1715+
1716+ // Try to access the session as user2 - should fail.
1717+ resp2 := doRequest (pingReq , sessionID , "user2" )
1718+ defer resp2 .Body .Close ()
1719+ if resp2 .StatusCode != http .StatusForbidden {
1720+ body , _ := io .ReadAll (resp2 .Body )
1721+ t .Errorf ("expected status %d for user mismatch, got %d: %s" , http .StatusForbidden , resp2 .StatusCode , body )
1722+ }
1723+
1724+ // Access as original user1 should succeed.
1725+ resp3 := doRequest (pingReq , sessionID , "user1" )
1726+ defer resp3 .Body .Close ()
1727+ if resp3 .StatusCode != http .StatusOK {
1728+ body , _ := io .ReadAll (resp3 .Body )
1729+ t .Errorf ("expected status %d for matching user, got %d: %s" , http .StatusOK , resp3 .StatusCode , body )
1730+ }
1731+ }
1732+
16571733func TestStreamableGET (t * testing.T ) {
16581734 // This test checks the fix for problematic behavior described in #410:
16591735 // Hanging GET headers should be written immediately, even if there are no
0 commit comments