From d187002d875e11e5a3708cfd9e4a634f88f8815d Mon Sep 17 00:00:00 2001 From: King Star Date: Wed, 15 Jul 2026 06:20:43 +0800 Subject: [PATCH] [Server] Reject malformed MCP session ID headers (#384) * [Server] Reject malformed MCP session ID headers * [Server] Narrow invalid session ID exception handling * [Server] Keep UUID catch compatible with supported Symfony --- .../Transport/StreamableHttpTransport.php | 15 +++++++-- .../Transport/StreamableHttpTransportTest.php | 33 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/Server/Transport/StreamableHttpTransport.php b/src/Server/Transport/StreamableHttpTransport.php index a5f757c..cce2bd5 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -343,8 +343,19 @@ class StreamableHttpTransport extends BaseTransport private function handleRequest(ServerRequestInterface $request): ResponseInterface { $this->request = $request; - $sessionIdString = $request->getHeaderLine(self::SESSION_HEADER); - $this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null; + $sessionIdHeaders = $request->getHeader(self::SESSION_HEADER); + if (\count($sessionIdHeaders) > 1) { + return $this->createErrorResponse(Error::forInvalidRequest(self::SESSION_HEADER.' header must not be repeated.'), 400); + } + + $sessionIdString = $sessionIdHeaders[0] ?? ''; + + try { + $this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null; + // Symfony UID 5.4/6.4 throw the global parent; newer versions throw a namespaced subclass. + } catch (\InvalidArgumentException) { + return $this->createErrorResponse(Error::forInvalidRequest(self::SESSION_HEADER.' header must be a valid UUID.'), 400); + } return match ($request->getMethod()) { 'OPTIONS' => $this->handleOptionsRequest(), diff --git a/tests/Unit/Server/Transport/StreamableHttpTransportTest.php b/tests/Unit/Server/Transport/StreamableHttpTransportTest.php index 32479a0..22b7fef 100644 --- a/tests/Unit/Server/Transport/StreamableHttpTransportTest.php +++ b/tests/Unit/Server/Transport/StreamableHttpTransportTest.php @@ -85,6 +85,39 @@ final class StreamableHttpTransportTest extends TestCase $this->assertSame(400, $response->getStatusCode()); } + #[TestDox('malformed MCP session IDs are rejected as bad requests')] + public function testMalformedSessionIdHeaderReturnsBadRequest(): void + { + $request = $this->factory + ->createServerRequest('POST', 'http://localhost/') + ->withHeader('Host', 'localhost') + ->withHeader(StreamableHttpTransport::SESSION_HEADER, '{"not":"a-token"}'); + + $transport = new StreamableHttpTransport($request, $this->factory, $this->factory); + + $response = $transport->listen(); + + $this->assertSame(400, $response->getStatusCode()); + $this->assertStringContainsString(StreamableHttpTransport::SESSION_HEADER, (string) $response->getBody()); + } + + #[TestDox('duplicate MCP session ID headers are rejected as bad requests')] + public function testDuplicateSessionIdHeadersReturnBadRequest(): void + { + $request = $this->factory + ->createServerRequest('POST', 'http://localhost/') + ->withHeader('Host', 'localhost') + ->withHeader(StreamableHttpTransport::SESSION_HEADER, '2fb587fc-593f-47ce-9d9a-9c06f2b907a3') + ->withAddedHeader(StreamableHttpTransport::SESSION_HEADER, '5e583da8-a677-4446-b723-4ddbe00fda62'); + + $transport = new StreamableHttpTransport($request, $this->factory, $this->factory); + + $response = $transport->listen(); + + $this->assertSame(400, $response->getStatusCode()); + $this->assertStringContainsString('must not be repeated', (string) $response->getBody()); + } + #[TestDox('explicit empty middleware list disables defaults and emits a warning log')] public function testEmptyMiddlewareListDisablesDefaultsAndWarns(): void {