VYPR
Medium severity5.9NVD Advisory· Published Mar 27, 2026· Updated Apr 2, 2026

CVE-2026-33946

CVE-2026-33946

Description

MCP Ruby SDK is the official Ruby SDK for Model Context Protocol servers and clients. Prior to version 0.9.2, the Ruby SDK's streamable_http_transport.rb implementation contains a session hijacking vulnerability. An attacker who obtains a valid session ID can completely hijack the victim's Server-Sent Events (SSE) stream and intercept all real-time data. Version 0.9.2 contains a patch.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
mcpRubyGems
< 0.9.20.9.2

Affected products

1

Patches

1
db40143402d6

Reject duplicate SSE connections with 409 to prevent stream hijacking

2 files changed · +75 4
  • lib/mcp/server/transports/streamable_http_transport.rb+18 4 modified
    @@ -142,6 +142,7 @@ def handle_get(request)
     
               return missing_session_id_response unless session_id
               return session_not_found_response unless session_exists?(session_id)
    +          return session_already_connected_response if get_session_stream(session_id)
     
               setup_sse_stream(session_id)
             end
    @@ -315,6 +316,14 @@ def session_not_found_response
               [404, { "Content-Type" => "application/json" }, [{ error: "Session not found" }.to_json]]
             end
     
    +        def session_already_connected_response
    +          [
    +            409,
    +            { "Content-Type" => "application/json" },
    +            [{ error: "Conflict: Only one SSE stream is allowed per session" }.to_json],
    +          ]
    +        end
    +
             def setup_sse_stream(session_id)
               body = create_sse_body(session_id)
     
    @@ -329,17 +338,22 @@ def setup_sse_stream(session_id)
     
             def create_sse_body(session_id)
               proc do |stream|
    -            store_stream_for_session(session_id, stream)
    -            start_keepalive_thread(session_id)
    +            stored = store_stream_for_session(session_id, stream)
    +            start_keepalive_thread(session_id) if stored
               end
             end
     
             def store_stream_for_session(session_id, stream)
               @mutex.synchronize do
    -            if @sessions[session_id]
    -              @sessions[session_id][:stream] = stream
    +            session = @sessions[session_id]
    +            if session && !session[:stream]
    +              session[:stream] = stream
                 else
    +              # Either session was removed, or another request already established a stream.
                   stream.close
    +              # `stream.close` may return a truthy value depending on the stream class.
    +              # Explicitly return nil to guarantee a falsy return for callers.
    +              nil
                 end
               end
             end
    
  • test/mcp/server/transports/streamable_http_transport_test.rb+57 0 modified
    @@ -272,6 +272,63 @@ class StreamableHTTPTransportTest < ActiveSupport::TestCase
               assert_equal "Missing session ID", body["error"]
             end
     
    +        test "rejects duplicate SSE connection with 409" do
    +          # Create a session
    +          init_request = create_rack_request(
    +            "POST",
    +            "/",
    +            { "CONTENT_TYPE" => "application/json" },
    +            { jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
    +          )
    +          init_response = @transport.handle_request(init_request)
    +          session_id = init_response[1]["Mcp-Session-Id"]
    +
    +          # Simulate an active SSE stream by storing a stream object in the session
    +          mock_stream = StringIO.new
    +          @transport.instance_variable_get(:@sessions)[session_id][:stream] = mock_stream
    +
    +          # Attempt a second GET request for the same session
    +          get_request = create_rack_request(
    +            "GET",
    +            "/",
    +            { "HTTP_MCP_SESSION_ID" => session_id },
    +          )
    +
    +          response = @transport.handle_request(get_request)
    +          assert_equal 409, response[0]
    +          assert_equal({ "Content-Type" => "application/json" }, response[1])
    +
    +          body = JSON.parse(response[2][0])
    +          assert_equal "Conflict: Only one SSE stream is allowed per session", body["error"]
    +        end
    +
    +        test "store_stream_for_session does not overwrite existing stream (TOCTOU guard)" do
    +          # Create a session
    +          init_request = create_rack_request(
    +            "POST",
    +            "/",
    +            { "CONTENT_TYPE" => "application/json" },
    +            { jsonrpc: "2.0", method: "initialize", id: "init" }.to_json,
    +          )
    +          init_response = @transport.handle_request(init_request)
    +          session_id = init_response[1]["Mcp-Session-Id"]
    +
    +          # Establish stream A
    +          stream_a = StringIO.new
    +          @transport.send(:store_stream_for_session, session_id, stream_a)
    +          assert_equal stream_a, @transport.instance_variable_get(:@sessions)[session_id][:stream]
    +
    +          # Attempt to store stream B (simulating a racing request)
    +          stream_b = StringIO.new
    +          @transport.send(:store_stream_for_session, session_id, stream_b)
    +
    +          # Stream A should still be the active stream
    +          assert_equal stream_a, @transport.instance_variable_get(:@sessions)[session_id][:stream]
    +
    +          # Stream B should have been closed
    +          assert stream_b.closed?
    +        end
    +
             test "handles GET request with invalid session ID" do
               request = create_rack_request(
                 "GET",
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

11

News mentions

0

No linked articles in our index yet.