Summary
The HTTP client transport in mcp/sdk reads a Server-Sent Events stream incrementally and appends each 4 KiB chunk to an in-memory buffer ($this->sseBuffer .= $chunk) with no upper bound. The buffer is only flushed when an SSE event delimiter ("\n\n") appears.
A remote MCP server — the peer the client connects to — that streams response bytes without ever sending the delimiter makes $sseBuffer grow without limit until the client process exhausts its PHP memory_limit or is killed by the OS OOM-killer.
This is a denial-of-service against the MCP client: any server it talks to, or a network position that controls the server’s response body, can crash the client by withholding the event delimiter while streaming data.
What Shopmon recommends
Disable MCP_SERVER inside the shop .env if it is set, until mcp/sdk is outside >=0.5.0,<0.7.1.
Impact
Type
Denial of service (memory exhaustion / process crash) of the MCP client.
Who can trigger it
The remote MCP server endpoint the client connects to via HttpTransport, or any party that can control or inject into that server’s SSE response body (a man-in-the-middle on a plaintext endpoint, or a malicious or compromised server).
The buffer growth happens while the transport is reading the response stream, before a complete event is ever parsed.
Effect
A response stream of N bytes containing no "\n\n" drives the client’s resident buffer to track N. A few hundred MB of delimiter-free data is enough to kill a client running with a typical memory_limit.
Severity note
The source advisory suggests High (a remote server can reliably crash a connected client). Shopmon currently publishes this as Low via enrichment override.
Severity (suggested, maintainer to confirm): High — a remote server can reliably crash a connected client over the HTTP/SSE transport.
How input reaches the sink
- A client connects to a server over the HTTP transport by constructing
Mcp\Client\Transport\HttpTransportwith the server endpoint URL, then runs the connect/request loop. - The transport’s loop calls
tick()(line 182), which callsprocessSSEStream()(line 194) on each iteration. processSSEStream()reads up to 4096 bytes from the active SSE stream and appends them to$this->sseBuffer(line 203).- The buffer is only drained inside the
while (false !== ($pos = strpos($this->sseBuffer, "\n\n")))loop (line 207). If the server never emits"\n\n",strposnever matches, the buffer is never flushed, and it grows on everytick()until OOM.
203php private string $sseBuffer = ''; php private function processSSEStream(): void { if (null === $this->activeStream) { return; } if (!$this->activeStream->eof()) { $chunk = $this->activeStream->read(4096); if ('' !== $chunk) { $this->sseBuffer .= $chunk; // unbounded append } } 207 while (false !== ($pos = strpos($this->sseBuffer, "\n\n"))) { $event = substr($this->sseBuffer, 0, $pos); $this->sseBuffer = substr($this->sseBuffer, $pos + 2); if (!empty(trim($event))) { $this->processSSEEvent($event); } } if ($this->activeStream->eof() && empty($this->sseBuffer)) { $this->activeStream = null; } }
Proof of concept
End-to-end reproduction against the released Composer package.
Environment
macOS arm64, PHP 8.5.6 (cli), Composer 2.9.8. The package under test is the published release mcp/sdk v0.5.0 (the version that introduced this HTTP client transport), installed from Packagist — not a re-packaged tree.
Full reproduction steps are in the upstream GHSA. Shopmon has not added a shop-specific PoC.