GHSA-52jp-gj8w-j6xh MCP Ruby SDK: Unbounded session retention in StreamableHTTPTransport allows memory exhaustion via initialize flood
SummaryIn its default configuration, `MCP::Server::Transports::StreamableHTTPTransport` never expires sessions. Every successful `initialize` request stores a new `ServerSession` and a session record under a fresh UUID, and the only path that removes them is an explicit client-issued HTTP `DELETE`. An unauthenticated attacker can repeatedly initialize new sessions and immediately disconnect, forcing the server to retain an unbounded number of `ServerSession` objects until memory is exhausted.Affected component`lib/mcp/server/transports/streamable_http_transport.rb`:Line 27, constructor: `def initialize(server, stateless: false, enable_json_response: false, session_idle_timeout: nil)` — the default for `session_idle_timeout` is `nil`.Line 46: `start_reaper_thread if @session_idle_timeout` — when the timeout is `nil`, the reaper that prunes idle sessions is never started.Lines 604–643 (`handle_initialization`): every successful `initialize` inserts a new session record; the only removal sites are `handle_delete` (client-controlled) and stream-error paths.The project README acknowledges the insecure default (line 1605):By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).Per-session memory cost is non-trivial: each entry contains a `ServerSession` instance (with its own `Mutex`, `@in_flight` hash, capabilities hash, and server reference), a top-level hash entry under the session UUID, and per-pending-request `Queue` allocations.Proof of conceptServer (`session_poc_server.rb`)Starts the transport in its default configuration (no `session_idle_timeout`) and reports the in-memory session count plus process RSS every two seconds.Client (`session_poc_client.py`)Reproduction commandsObserved resultTested on macOS, Ruby 3.2.4, against the SDK's `main` branch.Server terminal:Client terminal:50,000 unique sessions are created and retained in 26.6 seconds from a single client. The session count remains pinned at 50,000 indefinitely, confirming that no reaper exists to free the records. Scaling the attack linearly (multiple clients, larger client-capability payloads, longer runtime) drives RSS until the worker is OOM-killed.Impact**Attacker requirements:** unauthenticated TCP reach of the MCP endpoint. No session, no credentials.**Effect:** memory-exhaustion denial of service. A sustained or distributed attacker can OOM the worker; on services that recycle workers, the attacker simply repeats. On multi-tenant gateways, one tenant can starve all others.**Affected deployments:** every deployment that does not opt into `session_idle_timeout`. Because the README presents this as an opt-in mitigation rather than a default, real-world deployments are likely to ship vulnerable.Suggested mitigationChange the default of `session_idle_timeout` to a finite value (e.g. 30 minutes) and document the change as a security default.Add a `max_sessions:` constructor option; reject `initialize` with HTTP 503 once the cap is reached.Track the time of the `initialize` POST separately from later request activity, and evict sessions whose GET SSE stream is never attached within N seconds.
CVSS v3.1 score: 5.3