[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
This commit is contained in:
King Star 2026-07-15 06:20:43 +08:00 • committed by GitHub
commit d187002d87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 46 additions and 2 deletions

View file

@ -343,8 +343,19 @@ class StreamableHttpTransport extends BaseTransport
private function handleRequest(ServerRequestInterface $request): ResponseInterface private function handleRequest(ServerRequestInterface $request): ResponseInterface
{ {
$this->request = $request; $this->request = $request;
$sessionIdString = $request->getHeaderLine(self::SESSION_HEADER); $sessionIdHeaders = $request->getHeader(self::SESSION_HEADER);
$this->sessionId = $sessionIdString ? Uuid::fromString($sessionIdString) : null; 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()) { return match ($request->getMethod()) {
'OPTIONS' => $this->handleOptionsRequest(), 'OPTIONS' => $this->handleOptionsRequest(),

View file

@ -85,6 +85,39 @@ final class StreamableHttpTransportTest extends TestCase
$this->assertSame(400, $response->getStatusCode()); $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')] #[TestDox('explicit empty middleware list disables defaults and emits a warning log')]
public function testEmptyMiddlewareListDisablesDefaultsAndWarns(): void public function testEmptyMiddlewareListDisablesDefaultsAndWarns(): void
{ {