Ruby SSE Session Poisoning
A GitHub Security Advisory (GHSA-5p9g-j988-pcwv) documents a session poisoning flaw in the Ruby MCP SDK's Streamable/SSE HTTP transport: the server never validates that a POST to /messages/{session_id} actually belongs to the caller, so an attacker who obtains a victim's session ID (e.g. via logs or network sniffing) can silently execute tool calls whose responses are delivered into the victim's own SSE stream. The advisory includes a working PoC (attacker/legitimate client scripts against the SDK's example streamable_http_server.rb), notes that csharp-sdk and go-sdk correctly bind sessions to user identity while ruby-sdk does not, and distinguishes the bug from the related "Stream Replacement" advisory (GHSA-qvqr-5cv7-wh35), which disconnects rather than poisons the victim's stream.
What it is
GHSA-5p9g-j988-pcwv, "Missing Session Ownership Validation," in the Ruby MCP SDK's Streamable/SSE HTTP transport. The advisory rates it Integrity: HIGH (attacker executes unauthorized tools and modifies state) and Availability: LOW (attacker can disrupt the victim's session with injected responses). It is a silent attack — the victim's stream stays connected and receives the attacker's tool responses as if they were legitimate.
How it works
The server processes a POST /messages/{session_id} (the advisory's comparison table writes the vector as POST /mcp) without checking that the caller owns that session. Any party holding a valid session ID can inject tool calls; the results are delivered into the victim's SSE stream, not the attacker's. The PoC drives the SDK's own examples/streamable_http_server.rb, whose notification_tool the clients invoke.
What to do
The advisory's remediation is to enforce session ownership per the MCP spec, which states that "MCP servers SHOULD bind session IDs to user-specific information." Ruby-sdk does not; csharp-sdk and go-sdk do, and the advisory points at their implementations as the reference pattern:
- csharp-sdk:
src/ModelContextProtocol.AspNetCore/SseHandler.cs(lines 36 and 95–98) - go-sdk:
mcp/streamable.go(lines 66 and 316–320)
For a Ruby deployment: validate that the POSTing caller is the session's owner before dispatching the tool call, binding the session ID to authenticated user identity rather than treating possession of the ID as authorization. The advisory names no fixed version.
Reproducing
The PoC ships as Python (not Ruby) client scripts — legitimate_client.py and attacker_client.py — requiring Python 3.8+ and the requests library:
ruby streamable_http_server.rb # Terminal 1
python3 legitimate_client.py # Terminal 2 (victim)
python3 attacker_client.py abc-123-def-456 # Terminal 3, using session ID from Terminal 1The victim's terminal then shows the attacker-injected response.
Caveats
This is distinct from GHSA-qvqr-5cv7-wh35 (Stream Replacement): that one targets the SSE connection via GET /mcp and disconnects the victim, whereas this one targets tool execution via POST and leaves the victim connected but poisoned. How the attacker obtains the session ID (sniffing, logs) is explicitly out of scope of the advisory's analysis.
Organisations named here
- GHSA-qvqr-5cv7-wh35
Technologies named here
- streamable_http_server.rb
- Ruby MCP SDK
- attacker_client.py
- legitimate_client.py
- Model Context Protocol
- Ruby
Techniques named here
- Session Poisoning
- Stream Replacement
Related in AI Security
