Описание
MCP Ruby SDK: Unbounded session retention in StreamableHTTPTransport allows memory exhaustion via initialize flood
Summary
In 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 forsession_idle_timeoutisnil. - Line 46:
start_reaper_thread if @session_idle_timeout— when the timeout isnil, the reaper that prunes idle sessions is never started. - Lines 604–643 (
handle_initialization): every successfulinitializeinserts a new session record; the only removal sites arehandle_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 concept
Server (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 commands
Observed result
Tested 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 mitigation
- Change the default of
session_idle_timeoutto a finite value (e.g. 30 minutes) and document the change as a security default. - Add a
max_sessions:constructor option; rejectinitializewith HTTP 503 once the cap is reached. - Track the time of the
initializePOST separately from later request activity, and evict sessions whose GET SSE stream is never attached within N seconds.
Ссылки
- https://github.com/modelcontextprotocol/ruby-sdk/security/advisories/GHSA-52jp-gj8w-j6xh
- https://nvd.nist.gov/vuln/detail/CVE-2026-67430
- https://github.com/modelcontextprotocol/ruby-sdk/commit/afb968c468c178c4d3294b423fcce250621692f4
- https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.23.0
Пакеты
mcp
<= 0.22.0
0.23.0
Связанные уязвимости
MCP Ruby SDK is the official Ruby SDK for Model Context Protocol servers and clients. Prior to 0.23.0, MCP::Server::Transports::StreamableHTTPTransport in the mcp gem does not expire sessions by default, so repeated initialize requests retain unbounded ServerSession objects and can exhaust process memory. This issue is fixed in version 0.23.0.