diff --git a/src/Server/Transport/StreamableHttpTransport.php b/src/Server/Transport/StreamableHttpTransport.php index ab84b092..7b475cf3 100644 --- a/src/Server/Transport/StreamableHttpTransport.php +++ b/src/Server/Transport/StreamableHttpTransport.php @@ -294,8 +294,18 @@ private static function normalizeMiddleware(iterable $middleware): array 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; + } 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 0f949604..b5c93617 100644 --- a/tests/Unit/Server/Transport/StreamableHttpTransportTest.php +++ b/tests/Unit/Server/Transport/StreamableHttpTransportTest.php @@ -85,6 +85,39 @@ public function testDefaultMiddlewareRejectsUnsupportedProtocolVersion(): void $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 {