REPORT / 01
Analysis Report · typescript-sdk v1.25.3 → v1.26.0 — CVE-2026-25536
Shared security patch analysis results
02 ·
Lifecycle actions
cancel · resume · skip · regenerate
03 ·
Share this analysis
copy link · embed report
03 ·
CVE Security Analysis & Writeups
ai-generated · per cve
Comprehensive security analysis generated by AI for each confirmed CVE match. Click on a CVE to view the detailed writeup including vulnerability background, technical details, patch analysis, and PoC guide.
CVE-2026-25536
NVD
AI-Generated Analysis
05 ·
Findings
filter · search · paginate
Showing 0 to 0 of 0 results
src/examples/server/elicitationFormExample.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/examples/server/elicitationFormExample.ts 2026-04-22 05:52:50.748127055 +0000+++ cache/typescript-sdk_v1.26.0/src/examples/server/elicitationFormExample.ts 2026-04-22 05:52:51.152156301 +0000@@ -14,308 +14,314 @@ import { isInitializeRequest } from '../../types.js'; import { createMcpExpressApp } from '../../server/express.js'; -// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults-// The validator supports format validation (email, date, etc.) if ajv-formats is installed-const mcpServer = new McpServer(- {- name: 'form-elicitation-example-server',- version: '1.0.0'- },- {- capabilities: {}- }-);--/**- * Example 1: Simple user registration tool- * Collects username, email, and password from the user- */-mcpServer.registerTool(- 'register_user',- {- description: 'Register a new user account by collecting their information',- inputSchema: {}- },- async () => {- try {- // Request user information through form elicitation- const result = await mcpServer.server.elicitInput({- mode: 'form',- message: 'Please provide your registration information:',- requestedSchema: {- type: 'object',- properties: {- username: {- type: 'string',- title: 'Username',- description: 'Your desired username (3-20 characters)',- minLength: 3,- maxLength: 20- },- email: {- type: 'string',- title: 'Email',- description: 'Your email address',- format: 'email'- },- password: {- type: 'string',- title: 'Password',- description: 'Your password (min 8 characters)',- minLength: 8- },- newsletter: {- type: 'boolean',- title: 'Newsletter',- description: 'Subscribe to newsletter?',- default: false- }- },- required: ['username', 'email', 'password']- }- });+// Factory to create a new MCP server per session.+// Each session needs its own server+transport pair to avoid cross-session contamination.+const getServer = () => {+ // Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults+ // The validator supports format validation (email, date, etc.) if ajv-formats is installed+ const mcpServer = new McpServer(+ {+ name: 'form-elicitation-example-server',+ version: '1.0.0'+ },+ {+ capabilities: {}+ }+ ); - // Handle the different possible actions- if (result.action === 'accept' && result.content) {- const { username, email, newsletter } = result.content as {- username: string;- email: string;- password: string;- newsletter?: boolean;- };+ /**+ * Example 1: Simple user registration tool+ * Collects username, email, and password from the user+ */+ mcpServer.registerTool(+ 'register_user',+ {+ description: 'Register a new user account by collecting their information',+ inputSchema: {}+ },+ async () => {+ try {+ // Request user information through form elicitation+ const result = await mcpServer.server.elicitInput({+ mode: 'form',+ message: 'Please provide your registration information:',+ requestedSchema: {+ type: 'object',+ properties: {+ username: {+ type: 'string',+ title: 'Username',+ description: 'Your desired username (3-20 characters)',+ minLength: 3,+ maxLength: 20+ },+ email: {+ type: 'string',+ title: 'Email',+ description: 'Your email address',+ format: 'email'+ },+ password: {+ type: 'string',+ title: 'Password',+ description: 'Your password (min 8 characters)',+ minLength: 8+ },+ newsletter: {+ type: 'boolean',+ title: 'Newsletter',+ description: 'Subscribe to newsletter?',+ default: false+ }+ },+ required: ['username', 'email', 'password']+ }+ }); + // Handle the different possible actions+ if (result.action === 'accept' && result.content) {+ const { username, email, newsletter } = result.content as {+ username: string;+ email: string;+ password: string;+ newsletter?: boolean;+ };++ return {+ content: [+ {+ type: 'text',+ text: `Registration successful!\n\nUsername: ${username}\nEmail: ${email}\nNewsletter: ${newsletter ? 'Yes' : 'No'}`+ }+ ]+ };+ } else if (result.action === 'decline') {+ return {+ content: [+ {+ type: 'text',+ text: 'Registration cancelled by user.'+ }+ ]+ };+ } else {+ return {+ content: [+ {+ type: 'text',+ text: 'Registration was cancelled.'+ }+ ]+ };+ }+ } catch (error) { return { content: [ { type: 'text',- text: `Registration successful!\n\nUsername: ${username}\nEmail: ${email}\nNewsletter: ${newsletter ? 'Yes' : 'No'}`+ text: `Registration failed: ${error instanceof Error ? error.message : String(error)}` }- ]+ ],+ isError: true+ };+ }+ }+ );++ /**+ * Example 2: Multi-step workflow with multiple form elicitation requests+ * Demonstrates how to collect information in multiple steps+ */+ mcpServer.registerTool(+ 'create_event',+ {+ description: 'Create a calendar event by collecting event details',+ inputSchema: {}+ },+ async () => {+ try {+ // Step 1: Collect basic event information+ const basicInfo = await mcpServer.server.elicitInput({+ mode: 'form',+ message: 'Step 1: Enter basic event information',+ requestedSchema: {+ type: 'object',+ properties: {+ title: {+ type: 'string',+ title: 'Event Title',+ description: 'Name of the event',+ minLength: 1+ },+ description: {+ type: 'string',+ title: 'Description',+ description: 'Event description (optional)'+ }+ },+ required: ['title']+ }+ });++ if (basicInfo.action !== 'accept' || !basicInfo.content) {+ return {+ content: [{ type: 'text', text: 'Event creation cancelled.' }]+ };+ }++ // Step 2: Collect date and time+ const dateTime = await mcpServer.server.elicitInput({+ mode: 'form',+ message: 'Step 2: Enter date and time',+ requestedSchema: {+ type: 'object',+ properties: {+ date: {+ type: 'string',+ title: 'Date',+ description: 'Event date',+ format: 'date'+ },+ startTime: {+ type: 'string',+ title: 'Start Time',+ description: 'Event start time (HH:MM)'+ },+ duration: {+ type: 'integer',+ title: 'Duration',+ description: 'Duration in minutes',+ minimum: 15,+ maximum: 480+ }+ },+ required: ['date', 'startTime', 'duration']+ }+ });++ if (dateTime.action !== 'accept' || !dateTime.content) {+ return {+ content: [{ type: 'text', text: 'Event creation cancelled.' }]+ };+ }++ // Combine all collected information+ const event = {+ ...basicInfo.content,+ ...dateTime.content };- } else if (result.action === 'decline') {+ return { content: [ { type: 'text',- text: 'Registration cancelled by user.'+ text: `Event created successfully!\n\n${JSON.stringify(event, null, 2)}` } ] };- } else {+ } catch (error) { return { content: [ { type: 'text',- text: 'Registration was cancelled.'+ text: `Event creation failed: ${error instanceof Error ? error.message : String(error)}` }- ]+ ],+ isError: true }; }- } catch (error) {- return {- content: [- {- type: 'text',- text: `Registration failed: ${error instanceof Error ? error.message : String(error)}`- }- ],- isError: true- }; }- }-);+ ); -/**- * Example 2: Multi-step workflow with multiple form elicitation requests- * Demonstrates how to collect information in multiple steps- */-mcpServer.registerTool(- 'create_event',- {- description: 'Create a calendar event by collecting event details',- inputSchema: {}- },- async () => {- try {- // Step 1: Collect basic event information- const basicInfo = await mcpServer.server.elicitInput({- mode: 'form',- message: 'Step 1: Enter basic event information',- requestedSchema: {- type: 'object',- properties: {- title: {- type: 'string',- title: 'Event Title',- description: 'Name of the event',- minLength: 1+ /**+ * Example 3: Collecting address information+ * Demonstrates validation with patterns and optional fields+ */+ mcpServer.registerTool(+ 'update_shipping_address',+ {+ description: 'Update shipping address with validation',+ inputSchema: {}+ },+ async () => {+ try {+ const result = await mcpServer.server.elicitInput({+ mode: 'form',+ message: 'Please provide your shipping address:',+ requestedSchema: {+ type: 'object',+ properties: {+ name: {+ type: 'string',+ title: 'Full Name',+ description: 'Recipient name',+ minLength: 1+ },+ street: {+ type: 'string',+ title: 'Street Address',+ minLength: 1+ },+ city: {+ type: 'string',+ title: 'City',+ minLength: 1+ },+ state: {+ type: 'string',+ title: 'State/Province',+ minLength: 2,+ maxLength: 2+ },+ zipCode: {+ type: 'string',+ title: 'ZIP/Postal Code',+ description: '5-digit ZIP code'+ },+ phone: {+ type: 'string',+ title: 'Phone Number (optional)',+ description: 'Contact phone number'+ } },- description: {- type: 'string',- title: 'Description',- description: 'Event description (optional)'- }- },- required: ['title']- }- });-- if (basicInfo.action !== 'accept' || !basicInfo.content) {- return {- content: [{ type: 'text', text: 'Event creation cancelled.' }]- };- }-- // Step 2: Collect date and time- const dateTime = await mcpServer.server.elicitInput({- mode: 'form',- message: 'Step 2: Enter date and time',- requestedSchema: {- type: 'object',- properties: {- date: {- type: 'string',- title: 'Date',- description: 'Event date',- format: 'date'- },- startTime: {- type: 'string',- title: 'Start Time',- description: 'Event start time (HH:MM)'- },- duration: {- type: 'integer',- title: 'Duration',- description: 'Duration in minutes',- minimum: 15,- maximum: 480- }- },- required: ['date', 'startTime', 'duration']- }- });-- if (dateTime.action !== 'accept' || !dateTime.content) {- return {- content: [{ type: 'text', text: 'Event creation cancelled.' }]- };- }-- // Combine all collected information- const event = {- ...basicInfo.content,- ...dateTime.content- };-- return {- content: [- {- type: 'text',- text: `Event created successfully!\n\n${JSON.stringify(event, null, 2)}`+ required: ['name', 'street', 'city', 'state', 'zipCode'] }- ]- };- } catch (error) {- return {- content: [- {- type: 'text',- text: `Event creation failed: ${error instanceof Error ? error.message : String(error)}`- }- ],- isError: true- };- }- }-);+ }); -/**- * Example 3: Collecting address information- * Demonstrates validation with patterns and optional fields- */-mcpServer.registerTool(- 'update_shipping_address',- {- description: 'Update shipping address with validation',- inputSchema: {}- },- async () => {- try {- const result = await mcpServer.server.elicitInput({- mode: 'form',- message: 'Please provide your shipping address:',- requestedSchema: {- type: 'object',- properties: {- name: {- type: 'string',- title: 'Full Name',- description: 'Recipient name',- minLength: 1- },- street: {- type: 'string',- title: 'Street Address',- minLength: 1- },- city: {- type: 'string',- title: 'City',- minLength: 1- },- state: {- type: 'string',- title: 'State/Province',- minLength: 2,- maxLength: 2- },- zipCode: {- type: 'string',- title: 'ZIP/Postal Code',- description: '5-digit ZIP code'- },- phone: {- type: 'string',- title: 'Phone Number (optional)',- description: 'Contact phone number'- }- },- required: ['name', 'street', 'city', 'state', 'zipCode']+ if (result.action === 'accept' && result.content) {+ return {+ content: [+ {+ type: 'text',+ text: `Address updated successfully!\n\n${JSON.stringify(result.content, null, 2)}`+ }+ ]+ };+ } else if (result.action === 'decline') {+ return {+ content: [{ type: 'text', text: 'Address update cancelled by user.' }]+ };+ } else {+ return {+ content: [{ type: 'text', text: 'Address update was cancelled.' }]+ }; }- });-- if (result.action === 'accept' && result.content) {+ } catch (error) { return { content: [ { type: 'text',- text: `Address updated successfully!\n\n${JSON.stringify(result.content, null, 2)}`+ text: `Address update failed: ${error instanceof Error ? error.message : String(error)}` }- ]- };- } else if (result.action === 'decline') {- return {- content: [{ type: 'text', text: 'Address update cancelled by user.' }]- };- } else {- return {- content: [{ type: 'text', text: 'Address update was cancelled.' }]+ ],+ isError: true }; }- } catch (error) {- return {- content: [- {- type: 'text',- text: `Address update failed: ${error instanceof Error ? error.message : String(error)}`- }- ],- isError: true- }; }- }-);+ );++ return mcpServer;+}; async function main() { const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000;@@ -357,7 +363,8 @@ } }; - // Connect the transport to the MCP server BEFORE handling the request+ // Create a new server per session and connect it to the transport+ const mcpServer = getServer(); await mcpServer.connect(transport); await transport.handleRequest(req, res, req.body);
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Cross-session contamination src/examples/server/elicitationFormExample.ts lines 14-363
Old Code
// Create MCP server - it will automatically use AjvJsonSchemaValidator with sensible defaults
// The validator supports format validation (email, date, etc.) if ajv-formats is installed
const mcpServer = new McpServer(
{
name: 'form-elicitation-example-server',
version: '1.0.0'
},
{
capabilities: {}
}
);
...
// Connect the transport to the MCP server BEFORE handling the request
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
Fixed Code
// Factory to create a new MCP server per session.
// Each session needs its own server+transport pair to avoid cross-session contamination.
const getServer = () => {
const mcpServer = new McpServer(
{
name: 'form-elicitation-example-server',
version: '1.0.0'
},
{
capabilities: {}
}
);
...
return mcpServer;
};
...
// Create a new server per session and connect it to the transport
const mcpServer = getServer();
await mcpServer.connect(transport);
await transport.handleRequest(req, res, req.body);
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
src/examples/server/honoWebStandardStreamableHttp.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/examples/server/honoWebStandardStreamableHttp.ts 2026-04-22 05:52:50.748127055 +0000+++ cache/typescript-sdk_v1.26.0/src/examples/server/honoWebStandardStreamableHttp.ts 2026-04-22 05:52:51.152156301 +0000@@ -15,29 +15,30 @@ import { WebStandardStreamableHTTPServerTransport } from '../../server/webStandardStreamableHttp.js'; import { CallToolResult } from '../../types.js'; -// Create the MCP server-const server = new McpServer({- name: 'hono-webstandard-mcp-server',- version: '1.0.0'-});+// Factory function to create a new MCP server per request (stateless mode)+const getServer = () => {+ const server = new McpServer({+ name: 'hono-webstandard-mcp-server',+ version: '1.0.0'+ }); -// Register a simple greeting tool-server.registerTool(- 'greet',- {- title: 'Greeting Tool',- description: 'A simple greeting tool',- inputSchema: { name: z.string().describe('Name to greet') }- },- async ({ name }): Promise<CallToolResult> => {- return {- content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]- };- }-);+ // Register a simple greeting tool+ server.registerTool(+ 'greet',+ {+ title: 'Greeting Tool',+ description: 'A simple greeting tool',+ inputSchema: { name: z.string().describe('Name to greet') }+ },+ async ({ name }): Promise<CallToolResult> => {+ return {+ content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]+ };+ }+ ); -// Create a stateless transport (no options = no session management)-const transport = new WebStandardStreamableHTTPServerTransport();+ return server;+}; // Create the Hono app const app = new Hono();@@ -56,19 +57,22 @@ // Health check endpoint app.get('/health', c => c.json({ status: 'ok' })); -// MCP endpoint-app.all('/mcp', c => transport.handleRequest(c.req.raw));+// MCP endpoint - create a fresh transport and server per request (stateless)+app.all('/mcp', async c => {+ const transport = new WebStandardStreamableHTTPServerTransport();+ const server = getServer();+ await server.connect(transport);+ return transport.handleRequest(c.req.raw);+}); // Start the server const PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; -server.connect(transport).then(() => {- console.log(`Starting Hono MCP server on port ${PORT}`);- console.log(`Health check: http://localhost:${PORT}/health`);- console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);-- serve({- fetch: app.fetch,- port: PORT- });+console.log(`Starting Hono MCP server on port ${PORT}`);+console.log(`Health check: http://localhost:${PORT}/health`);+console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);++serve({+ fetch: app.fetch,+ port: PORT });
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Improper Session Isolation src/examples/server/honoWebStandardStreamableHttp.ts Lines 15-57
Old Code
// Create the MCP server
const server = new McpServer({
name: 'hono-webstandard-mcp-server',
version: '1.0.0'
});
// Register a simple greeting tool
server.registerTool(
'greet',
{
title: 'Greeting Tool',
description: 'A simple greeting tool',
inputSchema: { name: z.string().describe('Name to greet') }
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]
};
}
);
// Create a stateless transport (no options = no session management)
const transport = new WebStandardStreamableHTTPServerTransport();
// MCP endpoint
app.all('/mcp', c => transport.handleRequest(c.req.raw));
server.connect(transport).then(() => {
console.log(`Starting Hono MCP server on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
serve({
fetch: app.fetch,
port: PORT
});
});
Fixed Code
// Factory function to create a new MCP server per request (stateless mode)
const getServer = () => {
const server = new McpServer({
name: 'hono-webstandard-mcp-server',
version: '1.0.0'
});
// Register a simple greeting tool
server.registerTool(
'greet',
{
title: 'Greeting Tool',
description: 'A simple greeting tool',
inputSchema: { name: z.string().describe('Name to greet') }
},
async ({ name }): Promise<CallToolResult> => {
return {
content: [{ type: 'text', text: `Hello, ${name}! (from Hono + WebStandard transport)` }]
};
}
);
return server;
};
app.all('/mcp', async c => {
const transport = new WebStandardStreamableHTTPServerTransport();
const server = getServer();
await server.connect(transport);
return transport.handleRequest(c.req.raw);
});
console.log(`Starting Hono MCP server on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/health`);
console.log(`MCP endpoint: http://localhost:${PORT}/mcp`);
serve({
fetch: app.fetch,
port: PORT
});
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
src/examples/server/ssePollingExample.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/examples/server/ssePollingExample.ts 2026-04-22 05:52:50.748127055 +0000+++ cache/typescript-sdk_v1.26.0/src/examples/server/ssePollingExample.ts 2026-04-22 05:52:51.152156301 +0000@@ -12,7 +12,7 @@ * Run with: npx tsx src/examples/server/ssePollingExample.ts * Test with: curl or the MCP Inspector */-import { Request, Response } from 'express';+import { type Request, type Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { createMcpExpressApp } from '../../server/express.js';@@ -21,87 +21,92 @@ import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; import cors from 'cors'; -// Create the MCP server-const server = new McpServer(- {- name: 'sse-polling-example',- version: '1.0.0'- },- {- capabilities: { logging: {} }- }-);--// Register a long-running tool that demonstrates server-initiated disconnect-server.tool(- 'long-task',- 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',- {},- async (_args, extra): Promise<CallToolResult> => {- const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));-- console.log(`[${extra.sessionId}] Starting long-task...`);-- // Send first progress notification- await server.sendLoggingMessage(- {- level: 'info',- data: 'Progress: 25% - Starting work...'- },- extra.sessionId- );- await sleep(1000);-- // Send second progress notification- await server.sendLoggingMessage(- {- level: 'info',- data: 'Progress: 50% - Halfway there...'- },- extra.sessionId- );- await sleep(1000);-- // Server decides to disconnect the client to free resources- // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval- // Use extra.closeSSEStream callback - available when eventStore is configured- if (extra.closeSSEStream) {- console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);- extra.closeSSEStream();+// Factory to create a new MCP server per session.+// Each session needs its own server+transport pair to avoid cross-session contamination.+const getServer = () => {+ const server = new McpServer(+ {+ name: 'sse-polling-example',+ version: '1.0.0'+ },+ {+ capabilities: { logging: {} } }+ ); - // Continue processing while client is disconnected- // Events are stored in eventStore and will be replayed on reconnect- await sleep(500);- await server.sendLoggingMessage(- {- level: 'info',- data: 'Progress: 75% - Almost done (sent while client disconnected)...'- },- extra.sessionId- );-- await sleep(500);- await server.sendLoggingMessage(- {- level: 'info',- data: 'Progress: 100% - Complete!'- },- extra.sessionId- );+ // Register a long-running tool that demonstrates server-initiated disconnect+ server.tool(+ 'long-task',+ 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',+ {},+ async (_args, extra): Promise<CallToolResult> => {+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - console.log(`[${extra.sessionId}] Task complete`);+ console.log(`[${extra.sessionId}] Starting long-task...`); - return {- content: [+ // Send first progress notification+ await server.sendLoggingMessage( {- type: 'text',- text: 'Long task completed successfully!'- }- ]- };- }-);+ level: 'info',+ data: 'Progress: 25% - Starting work...'+ },+ extra.sessionId+ );+ await sleep(1000);++ // Send second progress notification+ await server.sendLoggingMessage(+ {+ level: 'info',+ data: 'Progress: 50% - Halfway there...'+ },+ extra.sessionId+ );+ await sleep(1000);++ // Server decides to disconnect the client to free resources+ // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval+ // Use extra.closeSSEStream callback - available when eventStore is configured+ if (extra.closeSSEStream) {+ console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);+ extra.closeSSEStream();+ }++ // Continue processing while client is disconnected+ // Events are stored in eventStore and will be replayed on reconnect+ await sleep(500);+ await server.sendLoggingMessage(+ {+ level: 'info',+ data: 'Progress: 75% - Almost done (sent while client disconnected)...'+ },+ extra.sessionId+ );++ await sleep(500);+ await server.sendLoggingMessage(+ {+ level: 'info',+ data: 'Progress: 100% - Complete!'+ },+ extra.sessionId+ );++ console.log(`[${extra.sessionId}] Task complete`);++ return {+ content: [+ {+ type: 'text',+ text: 'Long task completed successfully!'+ }+ ]+ };+ }+ );++ return server;+}; // Set up Express app const app = createMcpExpressApp();@@ -131,7 +136,8 @@ } }); - // Connect the MCP server to the transport+ // Create a new server per session and connect it to the transport+ const server = getServer(); await server.connect(transport); }
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Improper Session Isolation src/examples/server/ssePollingExample.ts Lines 21-136
[Old Code]
-import { Request, Response } from 'express';
+import { type Request, type Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '../../server/mcp.js';
import { createMcpExpressApp } from '../../server/express.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
import cors from 'cors';
-// Create the MCP server
-const server = new McpServer(
- {
- name: 'sse-polling-example',
- version: '1.0.0'
- },
- {
- capabilities: { logging: {} }
- }
-);
-
-// Register a long-running tool that demonstrates server-initiated disconnect
-server.tool(
- 'long-task',
- 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',
- {},
- async (_args, extra): Promise<CallToolResult> => {
- const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
-
- console.log(`[${extra.sessionId}] Starting long-task...`);
-
- // Send first progress notification
- await server.sendLoggingMessage(
- {
- level: 'info',
- data: 'Progress: 25% - Starting work...'
- },
- extra.sessionId
- );
- await sleep(1000);
-
- // Send second progress notification
- await server.sendLoggingMessage(
- {
- level: 'info',
- data: 'Progress: 50% - Halfway there...'
- },
- extra.sessionId
- );
- await sleep(1000);
-
- // Server decides to disconnect the client to free resources
- // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
- // Use extra.closeSSEStream callback - available when eventStore is configured
- if (extra.closeSSEStream) {
- console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
- extra.closeSSEStream();
-
- // Continue processing while client is disconnected
- // Events are stored in eventStore and will be replayed on reconnect
- await sleep(500);
- await server.sendLoggingMessage(
- {
- level: 'info',
- data: 'Progress: 75% - Almost done (sent while client disconnected)...'
- },
- extra.sessionId
- );
-
- await sleep(500);
- await server.sendLoggingMessage(
- {
- level: 'info',
- data: 'Progress: 100% - Complete!'
- },
- extra.sessionId
- );
-
- console.log(`[${extra.sessionId}] Task complete`);
-
- return {
- content: [
- {
- type: 'text',
- text: 'Long task completed successfully!'
- }
- ]
- };
- }
-);
// Set up Express app
const app = createMcpExpressApp();
@@ -131,7 +136,8 @@
}
});
- // Connect the MCP server to the transport
+ // Create a new server per session and connect it to the transport
+ const server = getServer();
await server.connect(transport);
}
[Fixed Code]
-import { Request, Response } from 'express';
+import { type Request, type Response } from 'express';
import { randomUUID } from 'node:crypto';
import { McpServer } from '../../server/mcp.js';
import { createMcpExpressApp } from '../../server/express.js';
import { InMemoryEventStore } from '../shared/inMemoryEventStore.js';
import cors from 'cors';
+// Factory to create a new MCP server per session.
+// Each session needs its own server+transport pair to avoid cross-session contamination.
+const getServer = () => {
+ const server = new McpServer(
+ {
+ name: 'sse-polling-example',
+ version: '1.0.0'
+ },
+ {
+ capabilities: { logging: {} }
+ }
+ );
+
+ // Register a long-running tool that demonstrates server-initiated disconnect
+ server.tool(
+ 'long-task',
+ 'A long-running task that sends progress updates. Server will disconnect mid-task to demonstrate polling.',
+ {},
+ async (_args, extra): Promise<CallToolResult> => {
+ const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+
+ console.log(`[${extra.sessionId}] Starting long-task...`);
+
+ // Send first progress notification
+ await server.sendLoggingMessage(
+ {
+ level: 'info',
+ data: 'Progress: 25% - Starting work...'
+ },
+ extra.sessionId
+ );
+ await sleep(1000);
+
+ // Send second progress notification
+ await server.sendLoggingMessage(
+ {
+ level: 'info',
+ data: 'Progress: 50% - Halfway there...'
+ },
+ extra.sessionId
+ );
+ await sleep(1000);
+
+ // Server decides to disconnect the client to free resources
+ // Client will reconnect via GET with Last-Event-ID after the transport's retryInterval
+ // Use extra.closeSSEStream callback - available when eventStore is configured
+ if (extra.closeSSEStream) {
+ console.log(`[${extra.sessionId}] Closing SSE stream to trigger client polling...`);
+ extra.closeSSEStream();
+ }
+
+ // Continue processing while client is disconnected
+ // Events are stored in eventStore and will be replayed on reconnect
+ await sleep(500);
+ await server.sendLoggingMessage(
+ {
+ level: 'info',
+ data: 'Progress: 75% - Almost done (sent while client disconnected)...'
+ },
+ extra.sessionId
+ );
+
+ await sleep(500);
+ await server.sendLoggingMessage(
+ {
+ level: 'info',
+ data: 'Progress: 100% - Complete!'
+ },
+ extra.sessionId
+ );
+
+ console.log(`[${extra.sessionId}] Task complete`);
+
+ return {
+ content: [
+ {
+ type: 'text',
+ text: 'Long task completed successfully!'
+ }
+ ]
+ };
+ }
+ );
+
+ return server;
+};
// Set up Express app
const app = createMcpExpressApp();
@@ -131,7 +136,8 @@
}
});
- // Connect the MCP server to the transport
+ // Create a new server per session and connect it to the transport
+ const server = getServer();
await server.connect(transport);
}
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
src/examples/server/standaloneSseWithGetStreamableHttp.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/examples/server/standaloneSseWithGetStreamableHttp.ts 2026-04-22 05:52:50.748127055 +0000+++ cache/typescript-sdk_v1.26.0/src/examples/server/standaloneSseWithGetStreamableHttp.ts 2026-04-22 05:52:51.152156301 +0000@@ -5,35 +5,46 @@ import { isInitializeRequest, ReadResourceResult } from '../../types.js'; import { createMcpExpressApp } from '../../server/express.js'; -// Create an MCP server with implementation details-const server = new McpServer({- name: 'resource-list-changed-notification-server',- version: '1.0.0'-});+// Factory to create a new MCP server per session.+// Each session needs its own server+transport pair to avoid cross-session contamination.+const getServer = () => {+ const server = new McpServer({+ name: 'resource-list-changed-notification-server',+ version: '1.0.0'+ });++ const addResource = (name: string, content: string) => {+ const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;+ server.registerResource(+ name,+ uri,+ { mimeType: 'text/plain', description: `Dynamic resource: ${name}` },+ async (): Promise<ReadResourceResult> => {+ return {+ contents: [{ uri, text: content }]+ };+ }+ );+ };++ addResource('example-resource', 'Initial content for example-resource');++ // Periodically add new resources to demonstrate notifications+ const resourceChangeInterval = setInterval(() => {+ const name = randomUUID();+ addResource(name, `Content for ${name}`);+ }, 5000);++ // Clean up the interval when the server closes+ server.server.onclose = () => {+ clearInterval(resourceChangeInterval);+ }; -// Store transports by session ID to send notifications-const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};--const addResource = (name: string, content: string) => {- const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;- server.registerResource(- name,- uri,- { mimeType: 'text/plain', description: `Dynamic resource: ${name}` },- async (): Promise<ReadResourceResult> => {- return {- contents: [{ uri, text: content }]- };- }- );+ return server; }; -addResource('example-resource', 'Initial content for example-resource');--const resourceChangeInterval = setInterval(() => {- const name = randomUUID();- addResource(name, `Content for ${name}`);-}, 5000); // Change resources every 5 seconds for testing+// Store transports by session ID to send notifications+const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {}; const app = createMcpExpressApp(); @@ -59,7 +70,8 @@ } }); - // Connect the transport to the MCP server+ // Create a new server per session and connect it to the transport+ const server = getServer(); await server.connect(transport); // Handle the request - the onsessioninitialized callback will store the transport@@ -121,7 +133,9 @@ // Handle server shutdown process.on('SIGINT', async () => { console.log('Shutting down server...');- clearInterval(resourceChangeInterval);- await server.close();+ for (const sessionId in transports) {+ await transports[sessionId].close();+ delete transports[sessionId];+ } process.exit(0); });
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Cross-session contamination / session isolation [src/examples/server/standaloneSseWithGetStreamableHttp.ts] [5-46, 59-133]
Old Code
// Create an MCP server with implementation details
const server = new McpServer({
name: 'resource-list-changed-notification-server',
version: '1.0.0'
});
// Store transports by session ID to send notifications
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
const addResource = (name: string, content: string) => {
const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
server.registerResource(
name,
uri,
{ mimeType: 'text/plain', description: `Dynamic resource: ${name}` },
async (): Promise<ReadResourceResult> => {
return {
contents: [{ uri, text: content }]
};
}
);
};
addResource('example-resource', 'Initial content for example-resource');
const resourceChangeInterval = setInterval(() => {
const name = randomUUID();
addResource(name, `Content for ${name}`);
}, 5000); // Change resources every 5 seconds for testing
...
// Connect the transport to the MCP server
await server.connect(transport);
...
process.on('SIGINT', async () => {
console.log('Shutting down server...');
clearInterval(resourceChangeInterval);
await server.close();
});
Fixed Code
// Factory to create a new MCP server per session.
// Each session needs its own server+transport pair to avoid cross-session contamination.
const getServer = () => {
const server = new McpServer({
name: 'resource-list-changed-notification-server',
version: '1.0.0'
});
const addResource = (name: string, content: string) => {
const uri = `https://mcp-example.com/dynamic/${encodeURIComponent(name)}`;
server.registerResource(
name,
uri,
{ mimeType: 'text/plain', description: `Dynamic resource: ${name}` },
async (): Promise<ReadResourceResult> => {
return {
contents: [{ uri, text: content }]
};
}
);
};
addResource('example-resource', 'Initial content for example-resource');
const resourceChangeInterval = setInterval(() => {
const name = randomUUID();
addResource(name, `Content for ${name}`);
}, 5000);
server.server.onclose = () => {
clearInterval(resourceChangeInterval);
};
return server;
};
// Store transports by session ID to send notifications
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
...
// Create a new server per session and connect it to the transport
const server = getServer();
await server.connect(transport);
...
process.on('SIGINT', async () => {
console.log('Shutting down server...');
for (const sessionId in transports) {
await transports[sessionId].close();
delete transports[sessionId];
}
process.exit(0);
});
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
src/server/webStandardStreamableHttp.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/server/webStandardStreamableHttp.ts 2026-04-22 05:52:50.756127634 +0000+++ cache/typescript-sdk_v1.26.0/src/server/webStandardStreamableHttp.ts 2026-04-22 05:52:51.156156591 +0000@@ -210,6 +210,7 @@ // when sessionId is not set (undefined), it means the transport is in stateless mode private sessionIdGenerator: (() => string) | undefined; private _started: boolean = false;+ private _hasHandledRequest: boolean = false; private _streamMapping: Map<string, StreamMapping> = new Map(); private _requestToStreamMapping: Map<RequestId, string> = new Map(); private _requestResponseMap: Map<RequestId, JSONRPCMessage> = new Map();@@ -319,6 +320,13 @@ * Returns a Response object (Web Standard) */ async handleRequest(req: Request, options?: HandleRequestOptions): Promise<Response> {+ // In stateless mode (no sessionIdGenerator), each request must use a fresh transport.+ // Reusing a stateless transport causes message ID collisions between clients.+ if (!this.sessionIdGenerator && this._hasHandledRequest) {+ throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');+ }+ this._hasHandledRequest = true;+ // Validate request headers for DNS rebinding protection const validationError = this.validateRequestHeaders(req); if (validationError) {
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Stateless Transport Reuse / Message ID Collision src/server/webStandardStreamableHttp.ts lines 210-320
Old Code:
private _started: boolean = false;
private _streamMapping: Map<string, StreamMapping> = new Map();
private _requestToStreamMapping: Map<RequestId, string> = new Map();
private _requestResponseMap: Map<RequestId, JSONRPCMessage> = new Map();
...
async handleRequest(req: Request, options?: HandleRequestOptions): Promise<Response> {
// Validate request headers for DNS rebinding protection
const validationError = this.validateRequestHeaders(req);
if (validationError) {
Fixed Code:
private _started: boolean = false;
private _hasHandledRequest: boolean = false;
private _streamMapping: Map<string, StreamMapping> = new Map();
private _requestToStreamMapping: Map<RequestId, string> = new Map();
private _requestResponseMap: Map<RequestId, JSONRPCMessage> = new Map();
...
async handleRequest(req: Request, options?: HandleRequestOptions): Promise<Response> {
// In stateless mode (no sessionIdGenerator), each request must use a fresh transport.
// Reusing a stateless transport causes message ID collisions between clients.
if (!this.sessionIdGenerator && this._hasHandledRequest) {
throw new Error('Stateless transport cannot be reused across requests. Create a new transport per request.');
}
this._hasHandledRequest = true;
// Validate request headers for DNS rebinding protection
const validationError = this.validateRequestHeaders(req);
if (validationError) {
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
src/shared/protocol.ts
AI: 2 vulnerabilities
2 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/src/shared/protocol.ts 2026-04-22 05:52:50.756127634 +0000+++ cache/typescript-sdk_v1.26.0/src/shared/protocol.ts 2026-04-22 05:52:51.156156591 +0000@@ -605,6 +605,12 @@ * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward. */ async connect(transport: Transport): Promise<void> {+ if (this._transport) {+ throw new Error(+ 'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'+ );+ }+ this._transport = transport; const _onclose = this.transport?.onclose; this._transport.onclose = () => {@@ -642,6 +648,12 @@ this._taskProgressTokens.clear(); this._pendingDebouncedNotifications.clear(); + // Abort all in-flight request handlers so they stop sending messages+ for (const controller of this._requestHandlerAbortControllers.values()) {+ controller.abort();+ }+ this._requestHandlerAbortControllers.clear();+ const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed'); this._transport = undefined;@@ -719,6 +731,7 @@ sessionId: capturedTransport?.sessionId, _meta: request.params?._meta, sendNotification: async notification => {+ if (abortController.signal.aborted) return; // Include related-task metadata if this request is part of a task const notificationOptions: NotificationOptions = { relatedRequestId: request.id }; if (relatedTaskId) {@@ -727,6 +740,9 @@ await this.notification(notification, notificationOptions); }, sendRequest: async (r, resultSchema, options?) => {+ if (abortController.signal.aborted) {+ throw new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled');+ } // Include related-task metadata if this request is part of a task const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id }; if (relatedTaskId && !requestOptions.relatedTask) {
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Improper Transport Reuse / Connection Re-establishment
File src/shared/protocol.ts
Lines 605-612
Old Code:
async connect(transport: Transport): Promise<void> {
this._transport = transport;
const _onclose = this.transport?.onclose;
this._transport.onclose = () => {
Fixed Code:
async connect(transport: Transport): Promise<void> {
if (this._transport) {
throw new Error(
'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'
);
}
this._transport = transport;
const _onclose = this.transport?.onclose;
this._transport.onclose = () => {
Vulnerability Existed: yes
TRUE POSITIVE
In-flight request handlers continuing after connection close
File src/shared/protocol.ts
Lines 642-648, 719-740
Old Code:
this._taskProgressTokens.clear();
this._pendingDebouncedNotifications.clear();
const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed');
...
sendNotification: async notification => {
const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
if (relatedTaskId) {
notificationOptions.relatedTask = relatedTaskId;
}
await this.notification(notification, notificationOptions);
},
sendRequest: async (r, resultSchema, options?) => {
const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
if (relatedTaskId && !requestOptions.relatedTask) {
Fixed Code:
this._taskProgressTokens.clear();
this._pendingDebouncedNotifications.clear();
// Abort all in-flight request handlers so they stop sending messages
for (const controller of this._requestHandlerAbortControllers.values()) {
controller.abort();
}
this._requestHandlerAbortControllers.clear();
const error = McpError.fromError(ErrorCode.ConnectionClosed, 'Connection closed');
...
sendNotification: async notification => {
if (abortController.signal.aborted) return;
const notificationOptions: NotificationOptions = { relatedRequestId: request.id };
if (relatedTaskId) {
notificationOptions.relatedTask = relatedTaskId;
}
await this.notification(notification, notificationOptions);
},
sendRequest: async (r, resultSchema, options?) => {
if (abortController.signal.aborted) {
throw new McpError(ErrorCode.ConnectionClosed, 'Request was cancelled');
}
const requestOptions: RequestOptions = { ...options, relatedRequestId: request.id };
if (relatedTaskId && !requestOptions.relatedTask)
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
test/integration-tests/stateManagementStreamableHttp.test.ts
AI: 1 vulnerabilities
1 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/test/integration-tests/stateManagementStreamableHttp.test.ts 2026-04-22 05:52:50.760127923 +0000+++ cache/typescript-sdk_v1.26.0/test/integration-tests/stateManagementStreamableHttp.test.ts 2026-04-22 05:52:51.160156880 +0000@@ -17,9 +17,11 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const { z } = entry; describe('Streamable HTTP Transport Session Management', () => {- // Function to set up the server with optional session management- async function setupServer(withSessionManagement: boolean) {- const server: Server = createServer();+ /**+ * Helper to create and configure a fresh McpServer instance with standard+ * resources, prompts, and tools for testing.+ */+ function createMcpServer(): McpServer { const mcpServer = new McpServer( { name: 'test-server', version: '1.0.0' }, {@@ -67,43 +69,67 @@ } ); - // Create transport with or without session management- const serverTransport = new StreamableHTTPServerTransport({- sessionIdGenerator: withSessionManagement- ? () => randomUUID() // With session management, generate UUID- : undefined // Without session management, return undefined- });+ return mcpServer;+ } - await mcpServer.connect(serverTransport);+ // Function to set up the server with optional session management+ async function setupServer(withSessionManagement: boolean): Promise<{+ server: Server;+ mcpServer?: McpServer;+ serverTransport?: StreamableHTTPServerTransport;+ baseUrl: URL;+ }> {+ const server: Server = createServer(); - server.on('request', async (req, res) => {- await serverTransport.handleRequest(req, res);- });+ if (withSessionManagement) {+ // Stateful mode: single transport + server for the session+ const mcpServer = createMcpServer();+ const serverTransport = new StreamableHTTPServerTransport({+ sessionIdGenerator: () => randomUUID()+ }); - // Start the server on a random port- const baseUrl = await listenOnRandomPort(server);+ await mcpServer.connect(serverTransport); - return { server, mcpServer, serverTransport, baseUrl };+ server.on('request', async (req, res) => {+ await serverTransport.handleRequest(req, res);+ });++ // Start the server on a random port+ const baseUrl = await listenOnRandomPort(server);++ return { server, mcpServer, serverTransport, baseUrl };+ } else {+ // Stateless mode: create a fresh transport + server per request+ // to comply with the guard that stateless transports cannot be reused.+ server.on('request', async (req, res) => {+ const mcpServer = createMcpServer();+ const serverTransport = new StreamableHTTPServerTransport({+ sessionIdGenerator: undefined+ });+ await mcpServer.connect(serverTransport);+ await serverTransport.handleRequest(req, res);+ // Close the per-request mcpServer after handling to avoid leaks+ await mcpServer.close();+ });++ // Start the server on a random port+ const baseUrl = await listenOnRandomPort(server);++ return { server, baseUrl };+ } } describe('Stateless Mode', () => { let server: Server;- let mcpServer: McpServer;- let serverTransport: StreamableHTTPServerTransport; let baseUrl: URL; beforeEach(async () => { const setup = await setupServer(false); server = setup.server;- mcpServer = setup.mcpServer;- serverTransport = setup.serverTransport; baseUrl = setup.baseUrl; }); afterEach(async () => {- // Clean up resources- await mcpServer.close().catch(() => {});- await serverTransport.close().catch(() => {}); server.close(); }); @@ -259,8 +285,8 @@ beforeEach(async () => { const setup = await setupServer(true); server = setup.server;- mcpServer = setup.mcpServer;- serverTransport = setup.serverTransport;+ mcpServer = setup.mcpServer!;+ serverTransport = setup.serverTransport!; baseUrl = setup.baseUrl; });
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Session state leakage via reused stateless transport test/integration-tests/stateManagementStreamableHttp.test.ts lines 17-109
Old Code
async function setupServer(withSessionManagement: boolean) {
const server: Server = createServer();
const mcpServer = new McpServer(
{ name: 'test-server', version: '1.0.0' },
{ capabilities: { logging: {}, tools: {}, resources: {}, prompts: {} } }
);
// Add a simple resource, prompt, and tool...
const serverTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: withSessionManagement
? () => randomUUID()
: undefined
});
await mcpServer.connect(serverTransport);
server.on('request', async (req, res) => {
await serverTransport.handleRequest(req, res);
});
const baseUrl = await listenOnRandomPort(server);
return { server, mcpServer, serverTransport, baseUrl };
}
Fixed Code
function createMcpServer(): McpServer {
const mcpServer = new McpServer(
{ name: 'test-server', version: '1.0.0' },
{ capabilities: { logging: {}, tools: {}, resources: {}, prompts: {} } }
);
// Add a simple resource, prompt, and tool...
return mcpServer;
}
async function setupServer(withSessionManagement: boolean): Promise<{
server: Server;
mcpServer?: McpServer;
serverTransport?: StreamableHTTPServerTransport;
baseUrl: URL;
}> {
const server: Server = createServer();
if (withSessionManagement) {
const mcpServer = createMcpServer();
const serverTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID()
});
await mcpServer.connect(serverTransport);
server.on('request', async (req, res) => {
await serverTransport.handleRequest(req, res);
});
const baseUrl = await listenOnRandomPort(server);
return { server, mcpServer, serverTransport, baseUrl };
} else {
server.on('request', async (req, res) => {
const mcpServer = createMcpServer();
const serverTransport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await mcpServer.connect(serverTransport);
await serverTransport.handleRequest(req, res);
await mcpServer.close();
});
const baseUrl = await listenOnRandomPort(server);
return { server, baseUrl };
}
}
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
test/server/streamableHttp.test.ts
AI: No vulnerabilities
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/test/server/streamableHttp.test.ts 2026-04-22 05:52:50.764128213 +0000+++ cache/typescript-sdk_v1.26.0/test/server/streamableHttp.test.ts 2026-04-22 05:52:51.164157170 +0000@@ -1529,20 +1529,56 @@ // Test stateless mode describe('StreamableHTTPServerTransport in stateless mode', () => { let server: Server;- let transport: StreamableHTTPServerTransport; let baseUrl: URL; + // In stateless mode, each request must use a fresh transport + server pair.+ // The HTTP server creates these per-request and delegates accordingly. beforeEach(async () => {- const result = await createTestServer({ sessionIdGenerator: undefined });- server = result.server;- transport = result.transport;- baseUrl = result.baseUrl;+ server = createServer(async (req, res) => {+ try {+ const { transport, mcpServer } = await createStatelessHandler();+ await transport.handleRequest(req, res);+ // Close the per-request mcpServer after handling to avoid leaks+ await mcpServer.close();+ } catch (error) {+ console.error('Error handling request:', error);+ if (!res.headersSent) res.writeHead(500).end();+ }+ });+ baseUrl = await listenOnRandomPort(server); }); afterEach(async () => {- await stopTestServer({ server, transport });+ server.close(); }); + /**+ * Creates a fresh transport + mcpServer pair for a single stateless request.+ */+ async function createStatelessHandler(): Promise<{+ transport: StreamableHTTPServerTransport;+ mcpServer: McpServer;+ }> {+ const mcpServer = new McpServer({ name: 'test-server', version: '1.0.0' }, { capabilities: { logging: {} } });++ mcpServer.tool(+ 'greet',+ 'A simple greeting tool',+ { name: z.string().describe('Name to greet') },+ async ({ name }): Promise<CallToolResult> => {+ return { content: [{ type: 'text', text: `Hello, ${name}!` }] };+ }+ );++ const transport = new StreamableHTTPServerTransport({+ sessionIdGenerator: undefined+ });++ await mcpServer.connect(transport);++ return { transport, mcpServer };+ }+ it('should operate without session ID validation', async () => { // Initialize the server first const initResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);@@ -1552,6 +1588,7 @@ expect(initResponse.headers.get('mcp-session-id')).toBeNull(); // Try request without session ID - should work in stateless mode+ // (a fresh transport is created per request) const toolsResponse = await sendPostRequest(baseUrl, TEST_MESSAGES.toolsList); expect(toolsResponse.status).toBe(200);@@ -1585,14 +1622,14 @@ expect(response2.status).toBe(200); }); - it('should reject second SSE stream even in stateless mode', async () => {- // Despite no session ID requirement, the transport still only allows- // one standalone SSE stream at a time+ it('should allow multiple SSE streams in stateless mode with per-request transports', async () => {+ // Each request gets its own transport, so multiple SSE streams can+ // coexist since they are handled by separate transport instances // Initialize the server first await sendPostRequest(baseUrl, TEST_MESSAGES.initialize); - // Open first SSE stream+ // Open first SSE stream - this uses its own per-request transport const stream1 = await fetch(baseUrl, { method: 'GET', headers: {@@ -1602,7 +1639,8 @@ }); expect(stream1.status).toBe(200); - // Open second SSE stream - should still be rejected, stateless mode still only allows one+ // Open second SSE stream - also gets its own per-request transport,+ // so it should also succeed (each transport only handles one request) const stream2 = await fetch(baseUrl, { method: 'GET', headers: {@@ -1610,7 +1648,9 @@ 'mcp-protocol-version': '2025-11-25' } });- expect(stream2.status).toBe(409); // Conflict - only one stream allowed+ // With per-request transports in stateless mode, each GET gets its own+ // transport, so the second one also succeeds+ expect(stream2.status).toBe(200); }); }); @@ -2868,17 +2908,20 @@ describe('Combined validations', () => { it('should validate both host and origin when both are configured', async () => {- const result = await createTestServerWithDnsProtection({+ // In stateless mode, each request needs a fresh transport, so we+ // test invalid and valid origins with separate server instances.++ // Test with invalid origin+ const result1 = await createTestServerWithDnsProtection({ sessionIdGenerator: undefined, allowedHosts: ['localhost'], allowedOrigins: ['http://localhost:3001'], enableDnsRebindingProtection: true });- server = result.server;- transport = result.transport;- baseUrl = result.baseUrl;+ server = result1.server;+ transport = result1.transport;+ baseUrl = result1.baseUrl; - // Test with invalid origin (host will be automatically correct via fetch) const response1 = await fetch(baseUrl, { method: 'POST', headers: {@@ -2893,7 +2936,20 @@ const body1 = await response1.json(); expect(body1.error.message).toBe('Invalid Origin header: http://evil.com'); - // Test with valid origin+ // Clean up first server+ await stopTestServer({ server, transport });++ // Test with valid origin using a fresh server+transport+ const result2 = await createTestServerWithDnsProtection({+ sessionIdGenerator: undefined,+ allowedHosts: ['localhost'],+ allowedOrigins: ['http://localhost:3001'],+ enableDnsRebindingProtection: true+ });+ server = result2.server;+ transport = result2.transport;+ baseUrl = result2.baseUrl;+ const response2 = await fetch(baseUrl, { method: 'POST', headers: {
AI Analysis
Vulnerability Existed: no
No vulnerability identified test/server/streamableHttp.test.ts 1529-2936
Old Code
beforeEach(async () => {
const result = await createTestServer({ sessionIdGenerator: undefined });
server = result.server;
transport = result.transport;
baseUrl = result.baseUrl;
});
afterEach(async () => {
await stopTestServer({ server, transport });
});
it('should reject second SSE stream even in stateless mode', async () => {
await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
const stream1 = await fetch(baseUrl, { method: 'GET', headers: { ... } });
expect(stream1.status).toBe(200);
const stream2 = await fetch(baseUrl, { method: 'GET', headers: { ... } });
expect(stream2.status).toBe(409);
});
Fixed Code
beforeEach(async () => {
server = createServer(async (req, res) => {
try {
const { transport, mcpServer } = await createStatelessHandler();
await transport.handleRequest(req, res);
await mcpServer.close();
} catch (error) {
console.error('Error handling request:', error);
if (!res.headersSent) res.writeHead(500).end();
}
});
baseUrl = await listenOnRandomPort(server);
});
afterEach(async () => {
server.close();
});
it('should allow multiple SSE streams in stateless mode with per-request transports', async () => {
await sendPostRequest(baseUrl, TEST_MESSAGES.initialize);
const stream1 = await fetch(baseUrl, { method: 'GET', headers: { ... } });
expect(stream1.status).toBe(200);
const stream2 = await fetch(baseUrl, { method: 'GET', headers: { ... } });
expect(stream2.status).toBe(200);
});
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
test/shared/protocol-transport-handling.test.ts
AI: 2 vulnerabilities
2 true positive(s)
CVE-2026-25536
--- cache/typescript-sdk_v1.25.3/test/shared/protocol-transport-handling.test.ts 2026-04-22 05:52:50.764128213 +0000+++ cache/typescript-sdk_v1.26.0/test/shared/protocol-transport-handling.test.ts 2026-04-22 05:52:51.164157170 +0000@@ -27,29 +27,39 @@ } } -describe('Protocol transport handling bug', () => {- let protocol: Protocol<Request, Notification, Result>;+function createProtocol(): Protocol<Request, Notification, Result> {+ return new (class extends Protocol<Request, Notification, Result> {+ protected assertCapabilityForMethod(): void {}+ protected assertNotificationCapability(): void {}+ protected assertRequestHandlerCapability(): void {}+ protected assertTaskCapability(): void {}+ protected assertTaskHandlerCapability(): void {}+ })();+}++describe('Protocol transport handling', () => { let transportA: MockTransport; let transportB: MockTransport; beforeEach(() => {- protocol = new (class extends Protocol<Request, Notification, Result> {- protected assertCapabilityForMethod(): void {}- protected assertNotificationCapability(): void {}- protected assertRequestHandlerCapability(): void {}- protected assertTaskCapability(): void {}- protected assertTaskHandlerCapability(): void {}- })();- transportA = new MockTransport('A'); transportB = new MockTransport('B'); }); - test('should send response to the correct transport when multiple clients are connected', async () => {- // Set up a request handler that simulates processing time- let resolveHandler: (value: Result) => void;- const handlerPromise = new Promise<Result>(resolve => {- resolveHandler = resolve;+ test('should send response to the correct transport when using separate protocol instances', async () => {+ const protocolA = createProtocol();+ const protocolB = createProtocol();++ // Each protocol gets its own resolver so we can verify responses route correctly+ let resolveA: (value: Result) => void;+ let resolveB: (value: Result) => void;+ let handlerAEnteredResolve: () => void;+ let handlerBEnteredResolve: () => void;+ const handlerAEntered = new Promise<void>(resolve => {+ handlerAEnteredResolve = resolve;+ });+ const handlerBEntered = new Promise<void>(resolve => {+ handlerBEnteredResolve = resolve; }); const TestRequestSchema = z.object({@@ -61,13 +71,22 @@ .optional() }); - protocol.setRequestHandler(TestRequestSchema, async request => {- console.log(`Processing request from ${request.params?.from}`);- return handlerPromise;+ protocolA.setRequestHandler(TestRequestSchema, async () => {+ return new Promise<Result>(resolve => {+ resolveA = resolve;+ handlerAEnteredResolve();+ });+ });++ protocolB.setRequestHandler(TestRequestSchema, async () => {+ return new Promise<Result>(resolve => {+ resolveB = resolve;+ handlerBEnteredResolve();+ }); }); // Client A connects and sends a request- await protocol.connect(transportA);+ await protocolA.connect(transportA); const requestFromA = { jsonrpc: '2.0' as const,@@ -79,9 +98,8 @@ // Simulate client A sending a request transportA.onmessage?.(requestFromA); - // While A's request is being processed, client B connects- // This overwrites the transport reference in the protocol- await protocol.connect(transportB);+ // Client B connects to a separate protocol instance+ await protocolB.connect(transportB); const requestFromB = { jsonrpc: '2.0' as const,@@ -93,19 +111,18 @@ // Client B sends its own request transportB.onmessage?.(requestFromB); - // Now complete A's request- resolveHandler!({ data: 'responseForA' } as Result);+ // Wait for both handlers to be invoked so resolvers are captured+ await handlerAEntered;+ await handlerBEntered;++ // Resolve each handler with distinct data+ resolveA!({ data: 'responseForA' } as Result);+ resolveB!({ data: 'responseForB' } as Result); - // Wait for async operations to complete+ // Wait for response delivery (transport.send is async) await new Promise(resolve => setTimeout(resolve, 10)); - // Check where the responses went- console.log('Transport A received:', transportA.sentMessages);- console.log('Transport B received:', transportB.sentMessages);-- // FIXED: Each transport now receives its own response-- // Transport A should receive response for request ID 1+ // Each transport receives its own response expect(transportA.sentMessages.length).toBe(1); expect(transportA.sentMessages[0]).toMatchObject({ jsonrpc: '2.0',@@ -113,18 +130,17 @@ result: { data: 'responseForA' } }); - // Transport B should only receive its own response (when implemented) expect(transportB.sentMessages.length).toBe(1); expect(transportB.sentMessages[0]).toMatchObject({ jsonrpc: '2.0', id: 2,- result: { data: 'responseForA' } // Same handler result in this test+ result: { data: 'responseForB' } }); }); - test('demonstrates the timing issue with multiple rapid connections', async () => {- const delays: number[] = [];- const results: { transport: string; response: JSONRPCMessage[] }[] = [];+ test('demonstrates isolation with separate protocol instances for rapid connections', async () => {+ const protocolA = createProtocol();+ const protocolB = createProtocol(); const DelayedRequestSchema = z.object({ method: z.literal('test/delayed'),@@ -136,21 +152,20 @@ .optional() }); - // Set up handler with variable delay- protocol.setRequestHandler(DelayedRequestSchema, async (request, extra) => {- const delay = request.params?.delay || 0;- delays.push(delay);-- await new Promise(resolve => setTimeout(resolve, delay));-- return {- processedBy: `handler-${extra.requestId}`,- delay: delay- } as Result;- });+ // Set up handler with variable delay on each protocol+ for (const protocol of [protocolA, protocolB]) {+ protocol.setRequestHandler(DelayedRequestSchema, async (request, extra) => {+ const delay = request.params?.delay || 0;+ await new Promise(resolve => setTimeout(resolve, delay));+ return {+ processedBy: `handler-${extra.requestId}`,+ delay: delay+ } as Result;+ });+ } - // Rapid succession of connections and requests- await protocol.connect(transportA);+ // Connect and send requests+ await protocolA.connect(transportA); transportA.onmessage?.({ jsonrpc: '2.0' as const, method: 'test/delayed',@@ -160,7 +175,7 @@ // Connect B while A is processing setTimeout(async () => {- await protocol.connect(transportB);+ await protocolB.connect(transportB); transportB.onmessage?.({ jsonrpc: '2.0' as const, method: 'test/delayed',@@ -172,18 +187,81 @@ // Wait for all processing await new Promise(resolve => setTimeout(resolve, 100)); - // Collect results- if (transportA.sentMessages.length > 0) {- results.push({ transport: 'A', response: transportA.sentMessages });- }- if (transportB.sentMessages.length > 0) {- results.push({ transport: 'B', response: transportB.sentMessages });- }-- console.log('Timing test results:', results);-- // FIXED: Each transport receives its own responses+ // Each transport receives its own responses expect(transportA.sentMessages.length).toBe(1); expect(transportB.sentMessages.length).toBe(1); });++ test('connect guard throws when calling connect() twice without closing', async () => {+ const protocol = createProtocol();++ await protocol.connect(transportA);++ await expect(protocol.connect(transportB)).rejects.toThrow(+ 'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'+ );+ });++ test('connect succeeds after calling close() first', async () => {+ const protocol = createProtocol();++ await protocol.connect(transportA);+ await protocol.close();++ // Should succeed without error+ await expect(protocol.connect(transportB)).resolves.toBeUndefined();+ });++ test('close() aborts in-flight request handlers', async () => {+ const protocol = createProtocol();++ const SlowRequestSchema = z.object({+ method: z.literal('test/slow')+ });++ let capturedSignal: AbortSignal | undefined;+ let capturedSendNotification: ((notification: Notification) => Promise<void>) | undefined;+ let resolveHandler: () => void;+ const handlerBlocking = new Promise<void>(resolve => {+ resolveHandler = resolve;+ });++ protocol.setRequestHandler(SlowRequestSchema, async (_request, extra) => {+ capturedSignal = extra.signal;+ capturedSendNotification = extra.sendNotification;+ // Block the handler until we release it+ await handlerBlocking;+ return {} as Result;+ });++ await protocol.connect(transportA);++ // Send a request to trigger the handler+ transportA.onmessage?.({+ jsonrpc: '2.0' as const,+ method: 'test/slow',+ id: 1+ });++ // Wait for the handler to start and capture the signal+ await new Promise(resolve => setTimeout(resolve, 10));+ expect(capturedSignal).toBeDefined();+ expect(capturedSignal!.aborted).toBe(false);++ // Close the protocol while the handler is still in-flight+ await protocol.close();++ // The signal should now be aborted+ expect(capturedSignal!.aborted).toBe(true);++ // sendNotification should be a no-op after close (no error thrown)+ await expect(capturedSendNotification!({ method: 'notifications/test' } as Notification)).resolves.toBeUndefined();++ // No notification should have been sent to the transport+ const notifications = transportA.sentMessages.filter((m: JSONRPCMessage) => 'method' in m && m.method === 'notifications/test');+ expect(notifications).toHaveLength(0);++ // Release the handler so the promise chain completes+ resolveHandler!();+ }); });
AI Analysis
Vulnerability Existed: yes
TRUE POSITIVE
Protocol transport misrouting [test/shared/protocol-transport-handling.test.ts] [61-93, 136-187]
Old Code
protocol.setRequestHandler(TestRequestSchema, async request => {
console.log(`Processing request from ${request.params?.from}`);
return handlerPromise;
});
await protocol.connect(transportA);
...
await protocol.connect(transportB);
...
resolveHandler!({ data: 'responseForA' } as Result);
Fixed Code
const protocolA = createProtocol();
const protocolB = createProtocol();
protocolA.setRequestHandler(TestRequestSchema, async () => {
return new Promise<Result>(resolve => {
resolveA = resolve;
handlerAEnteredResolve();
});
});
protocolB.setRequestHandler(TestRequestSchema, async () => {
return new Promise<Result>(resolve => {
resolveB = resolve;
handlerBEnteredResolve();
});
});
await protocolA.connect(transportA);
...
await protocolB.connect(transportB);
...
resolveA!({ data: 'responseForA' } as Result);
resolveB!({ data: 'responseForB' } as Result);
Vulnerability Existed: yes
TRUE POSITIVE
Protocol connect reuse and in-flight request cancellation [test/shared/protocol-transport-handling.test.ts] [187-260]
Old Code
// No existing tests or guard for calling connect() twice on the same Protocol instance
// No existing tests ensuring close() aborts in-flight request handlers
Fixed Code
test('connect guard throws when calling connect() twice without closing', async () => {
const protocol = createProtocol();
await protocol.connect(transportA);
await expect(protocol.connect(transportB)).rejects.toThrow(
'Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.'
);
});
test('connect succeeds after calling close() first', async () => {
const protocol = createProtocol();
await protocol.connect(transportA);
await protocol.close();
await expect(protocol.connect(transportB)).resolves.toBeUndefined();
});
test('close() aborts in-flight request handlers', async () => {
const protocol = createProtocol();
const SlowRequestSchema = z.object({ method: z.literal('test/slow') });
let capturedSignal: AbortSignal | undefined;
let capturedSendNotification: ((notification: Notification) => Promise<void>) | undefined;
let resolveHandler: () => void;
const handlerBlocking = new Promise<void>(resolve => { resolveHandler = resolve; });
protocol.setRequestHandler(SlowRequestSchema, async (_request, extra) => {
capturedSignal = extra.signal;
capturedSendNotification = extra.sendNotification;
await handlerBlocking;
return {} as Result;
});
await protocol.connect(transportA);
transportA.onmessage?.({ jsonrpc: '2.0' as const, method: 'test/slow', id: 1 });
await new Promise(resolve => setTimeout(resolve, 10));
expect(capturedSignal!.aborted).toBe(false);
await protocol.close();
expect(capturedSignal!.aborted).toBe(true);
await expect(capturedSendNotification!({ method: 'notifications/test' } as Notification)).resolves.toBeUndefined();
const notifications = transportA.sentMessages.filter((m: JSONRPCMessage) => 'method' in m && m.method === 'notifications/test');
expect(notifications).toHaveLength(0);
resolveHandler!();
});
CVE Analysis Results:
CVE-2026-25536: Yes
View CVE Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. From version 1.10.0 to 1.25.3, cross-client response data leak when a single McpServer/Server and transport instance is reused across multiple client connections, most commonly in stateless StreamableHTTPServerTransport deployments. This issue has been patched in version 1.26.0.
Showing 1 to 9 of 9 results