OSV 1.4.0 · github-reviewed · 修改于 2026-07-30 22:41
发布时间
2026-07-30 22:41
GitHub 审查时间
2026-07-30 22:41
NVD 发布时间
2026-07-30 04:17
源文件
advisories/github-reviewed/2026/07/GHSA-7683-3w9x-ch42/GHSA-7683-3w9x-ch42.json
The stdio transports in MCP::Server::Transports::StdioTransport and MCP::Client::Stdio read newline-delimited JSON-RPC frames using IO#gets with no limit argument. CRuby's IO#gets with no limit reads from the current position until the next separator (\n) with no upper bound on the returned string length. A peer that streams bytes without ever emitting a newline causes gets to accumulate the entire stream in a single Ruby String until the process is killed by the operating-system OOM killer.
This is the same vulnerability class tracked in sibling MCP SDKs as GHSA-74gp-qhv5-v493 (Kotlin), GHSA-wqgc-pwpr-pq7r (TypeScript), GHSA-655q-2283-6jgj (Python), and others. It was identified during a cross-SDK audit; ruby-sdk had no prior report.
Verified at main @ cf44475c.
lib/mcp/server/transports/stdio_transport.rb# line 23
while @open && (line = $stdin.gets) # <-- no limit argument
response = @session.handle_json(line.strip)
...
# line 76
while @open && (line = $stdin.gets) # <-- no limit argument
begin
parsed = JSON.parse(line.strip, symbolize_names: true)
$stdin is the raw process global (only set_encoding is applied at line 16); there is no wrapper imposing a length limit.
lib/mcp/client/stdio.rb# line 150
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args)
# line 228
line = @stdout.gets # <-- no limit argument
raise_connection_error!(method, params) if line.nil?
parsed = JSON.parse(line.strip)
@stdout is the raw IO returned by Open3.popen3. The guard at line 227 () only gates the before is invoked; once bytes are flowing, blocks indefinitely accumulating into one string.
@read_timeoutwait_for_readable!IO.selectgetsgetsDenial of service via memory exhaustion. A peer that controls the byte stream delivered to the stdio transport can grow a single Ruby String until the process exhausts available memory.
Threat-model caveat (important): In the default stdio deployment, the peer process already holds local execution privileges equal to or greater than the victim:
StdioTransport#open), the writer to $stdin is the parent process that spawned the server, which already holds Process.kill, environment control, and filesystem access over the child. This direction is a robustness defect rather than a security boundary in typical deployments.Client::Stdio#read_response), the writer is the third-party MCP server binary the host application chose to spawn via Open3.popen3. In an unsandboxed deployment that binary already has local code execution as the host user.This issue is primarily a security concern for:
gets lets a memory-capped sandboxed process exhaust the unconstrained host;The bug fires before the JSON-RPC initialize handshake, since gets cannot return until \n arrives.
# poc_ruby_stdio_oom.rb — drives the real client transport against a producer that never emits \n
require "mcp/client/stdio"
# `yes` writes "y\n" — instead use a producer that never sends \n:
producer = %q{ruby -e 'STDOUT.sync=true; loop { print "A" * 65536 }'}
client = MCP::Client::Stdio.new(command: "bash", args: ["-c", producer])
client.start
# any request triggers read_response → @stdout.gets → unbounded String growth
client.send_request(method: "initialize", params: {})
Observed: process RSS grows linearly with bytes produced; gets never returns; process is OOM-killed.
IO#gets accepts a second limit argument. Apply a configurable maximum line length (default suggested: 4 MiB — large enough for any realistic JSON-RPC frame including base64-embedded images) at all three call sites, and treat an over-limit line as a transport error that closes the connection:
MAX_LINE_BYTES = 4 * 1024 * 1024
while @open && (line = $stdin.gets("\n", MAX_LINE_BYTES))
unless line.end_with?("\n")
# gets returned because the limit was hit, not because a newline arrived
raise MCP::TransportError, "stdio frame exceeds #{MAX_LINE_BYTES} bytes without newline"
end
...
end
Apply the same pattern at stdio_transport.rb:76 and client/stdio.rb:228. Expose MAX_LINE_BYTES as a constructor option on both transports for callers with legitimate large-frame needs.
Identified during a cross-SDK audit of the stdio unbounded-buffer vulnerability class, prompted by GHSA-74gp-qhv5-v493 (reporter: tonghuaroot).