DNS Rebinding Protection Disabled by Default in Model Context Protocol TypeScript SDK for Servers Running on Localhost
Description
MCP TypeScript SDK is the official TypeScript SDK for Model Context Protocol servers and clients. Prior to 1.24.0, The Model Context Protocol (MCP) TypeScript SDK does not enable DNS rebinding protection by default for HTTP-based servers. When an HTTP-based MCP server is run on localhost without authentication with StreamableHTTPServerTransport or SSEServerTransport and has not enabled enableDnsRebindingProtection, a malicious website could exploit DNS rebinding to bypass same-origin policy restrictions and send requests to the local MCP server. This could allow an attacker to invoke tools or access resources exposed by the MCP server on behalf of the user in those limited circumstances. Note that running HTTP-based MCP servers locally without authentication is not recommended per MCP security best practices. This issue does not affect servers using stdio transport. This vulnerability is fixed in 1.24.0.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@modelcontextprotocol/sdknpm | < 1.24.0 | 1.24.0 |
Affected products
1- Range: < 1.24.0
Patches
209623e2aa504Merge commit from fork
16 files changed · +387 −77
docs/server.md+28 −0 modified@@ -66,6 +66,34 @@ For a minimal “getting started” experience: For more detailed patterns (stateless vs stateful, JSON response mode, CORS, DNS rebind protection), see the examples above and the MCP spec sections on transports. +## DNS rebinding protection + +MCP servers running on localhost are vulnerable to DNS rebinding attacks. Use `createMcpExpressApp()` to create an Express app with DNS rebinding protection enabled by default: + +```typescript +import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/index.js'; + +// Protection auto-enabled (default host is 127.0.0.1) +const app = createMcpExpressApp(); + +// Protection auto-enabled for localhost +const app = createMcpExpressApp({ host: 'localhost' }); + +// No auto protection when binding to all interfaces +const app = createMcpExpressApp({ host: '0.0.0.0' }); +``` + +For custom host validation, use the middleware directly: + +```typescript +import express from 'express'; +import { hostHeaderValidation } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js'; + +const app = express(); +app.use(express.json()); +app.use(hostHeaderValidation(['localhost', '127.0.0.1', 'myhost.local'])); +``` + ## Tools, resources, and prompts ### Tools
src/examples/server/elicitationFormExample.ts+3 −12 modified@@ -8,11 +8,11 @@ // to collect *sensitive* user input via a browser. import { randomUUID } from 'node:crypto'; -import cors from 'cors'; -import express, { type Request, type Response } from 'express'; +import { type Request, type Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest } from '../../types.js'; +import { createMcpExpressApp } from '../../server/index.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 @@ -320,16 +320,7 @@ mcpServer.registerTool( async function main() { const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : 3000; - const app = express(); - app.use(express.json()); - - // Allow CORS for all domains, expose the Mcp-Session-Id header - app.use( - cors({ - origin: '*', - exposedHeaders: ['Mcp-Session-Id'] - }) - ); + const app = createMcpExpressApp(); // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
src/examples/server/elicitationUrlExample.ts+2 −2 modified@@ -11,6 +11,7 @@ import express, { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { z } from 'zod'; import { McpServer } from '../../server/mcp.js'; +import { createMcpExpressApp } from '../../server/index.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; @@ -214,8 +215,7 @@ function completeURLElicitation(elicitationId: string) { const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; -const app = express(); -app.use(express.json()); +const app = createMcpExpressApp(); // Allow CORS all domains, expose the Mcp-Session-Id header app.use(
src/examples/server/jsonResponseStreamableHttp.ts+3 −12 modified@@ -1,10 +1,10 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; -import cors from 'cors'; +import { createMcpExpressApp } from '../../server/index.js'; // Create an MCP server with implementation details const getServer = () => { @@ -90,16 +90,7 @@ const getServer = () => { return server; }; -const app = express(); -app.use(express.json()); - -// Configure CORS to expose Mcp-Session-Id header for browser-based clients -app.use( - cors({ - origin: '*', // Allow all origins - adjust as needed for production - exposedHeaders: ['Mcp-Session-Id'] - }) -); +const app = createMcpExpressApp(); // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
src/examples/server/simpleSseServer.ts+3 −3 modified@@ -1,8 +1,9 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult } from '../../types.js'; +import { createMcpExpressApp } from '../../server/index.js'; /** * This example server demonstrates the deprecated HTTP+SSE transport @@ -75,8 +76,7 @@ const getServer = () => { return server; }; -const app = express(); -app.use(express.json()); +const app = createMcpExpressApp(); // Store transports by session ID const transports: Record<string, SSEServerTransport> = {};
src/examples/server/simpleStatelessStreamableHttp.ts+3 −12 modified@@ -1,9 +1,9 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import * as z from 'zod/v4'; import { CallToolResult, GetPromptResult, ReadResourceResult } from '../../types.js'; -import cors from 'cors'; +import { createMcpExpressApp } from '../../server/index.js'; const getServer = () => { // Create an MCP server with implementation details @@ -96,16 +96,7 @@ const getServer = () => { return server; }; -const app = express(); -app.use(express.json()); - -// Configure CORS to expose Mcp-Session-Id header for browser-based clients -app.use( - cors({ - origin: '*', // Allow all origins - adjust as needed for production - exposedHeaders: ['Mcp-Session-Id'] - }) -); +const app = createMcpExpressApp(); app.post('/mcp', async (req: Request, res: Response) => { const server = getServer();
src/examples/server/simpleStreamableHttp.ts+3 −13 modified@@ -1,10 +1,11 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import * as z from 'zod/v4'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from '../../server/auth/router.js'; import { requireBearerAuth } from '../../server/auth/middleware/bearerAuth.js'; +import { createMcpExpressApp } from '../../server/index.js'; import { CallToolResult, ElicitResultSchema, @@ -20,8 +21,6 @@ import { setupAuthServer } from './demoInMemoryOAuthProvider.js'; import { OAuthMetadata } from '../../shared/auth.js'; import { checkResourceAllowed } from '../../shared/auth-utils.js'; -import cors from 'cors'; - // Check for OAuth flag const useOAuth = process.argv.includes('--oauth'); const strictOAuth = process.argv.includes('--oauth-strict'); @@ -507,16 +506,7 @@ const getServer = () => { const MCP_PORT = process.env.MCP_PORT ? parseInt(process.env.MCP_PORT, 10) : 3000; const AUTH_PORT = process.env.MCP_AUTH_PORT ? parseInt(process.env.MCP_AUTH_PORT, 10) : 3001; -const app = express(); -app.use(express.json()); - -// Allow CORS all domains, expose the Mcp-Session-Id header -app.use( - cors({ - origin: '*', // Allow all origins - exposedHeaders: ['Mcp-Session-Id'] - }) -); +const app = createMcpExpressApp(); // Set up OAuth if enabled let authMiddleware = null;
src/examples/server/simpleTaskInteractive.ts+3 −4 modified@@ -9,9 +9,9 @@ * creates a task, and the result is fetched via tasks/result endpoint. */ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; -import { Server } from '../../server/index.js'; +import { createMcpExpressApp, Server } from '../../server/index.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult, @@ -630,8 +630,7 @@ const createServer = (): Server => { // Express App Setup // ============================================================================ -const app = express(); -app.use(express.json()); +const app = createMcpExpressApp(); // Map to store transports by session ID const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
src/examples/server/sseAndStreamableHttpCompatibleServer.ts+3 −12 modified@@ -1,12 +1,12 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { SSEServerTransport } from '../../server/sse.js'; import * as z from 'zod/v4'; import { CallToolResult, isInitializeRequest } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; -import cors from 'cors'; +import { createMcpExpressApp } from '../../server/index.js'; /** * This example server demonstrates backwards compatibility with both: @@ -71,16 +71,7 @@ const getServer = () => { }; // Create Express application -const app = express(); -app.use(express.json()); - -// Configure CORS to expose Mcp-Session-Id header for browser-based clients -app.use( - cors({ - origin: '*', // Allow all origins - adjust as needed for production - exposedHeaders: ['Mcp-Session-Id'] - }) -); +const app = createMcpExpressApp(); // Store transports by session ID const transports: Record<string, StreamableHTTPServerTransport | SSEServerTransport> = {};
src/examples/server/ssePollingExample.ts+5 −4 modified@@ -12,9 +12,10 @@ * Run with: npx tsx src/examples/server/ssePollingExample.ts * Test with: curl or the MCP Inspector */ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; +import { createMcpExpressApp } from '../../server/index.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { CallToolResult } from '../../types.js'; import { InMemoryEventStore } from '../shared/inMemoryEventStore.js'; @@ -103,7 +104,7 @@ server.tool( ); // Set up Express app -const app = express(); +const app = createMcpExpressApp(); app.use(cors()); // Create event store for resumability @@ -112,8 +113,8 @@ const eventStore = new InMemoryEventStore(); // Track transports by session ID for session reuse const transports = new Map<string, StreamableHTTPServerTransport>(); -// Handle all MCP requests - use express.json() only for this route -app.all('/mcp', express.json(), async (req: Request, res: Response) => { +// Handle all MCP requests +app.all('/mcp', async (req: Request, res: Response) => { const sessionId = req.headers['mcp-session-id'] as string | undefined; // Reuse existing transport or create new one
src/examples/server/standaloneSseWithGetStreamableHttp.ts+3 −3 modified@@ -1,8 +1,9 @@ -import express, { Request, Response } from 'express'; +import { Request, Response } from 'express'; import { randomUUID } from 'node:crypto'; import { McpServer } from '../../server/mcp.js'; import { StreamableHTTPServerTransport } from '../../server/streamableHttp.js'; import { isInitializeRequest, ReadResourceResult } from '../../types.js'; +import { createMcpExpressApp } from '../../server/index.js'; // Create an MCP server with implementation details const server = new McpServer({ @@ -34,8 +35,7 @@ const resourceChangeInterval = setInterval(() => { addResource(name, `Content for ${name}`); }, 5000); // Change resources every 5 seconds for testing -const app = express(); -app.use(express.json()); +const app = createMcpExpressApp(); app.post('/mcp', async (req: Request, res: Response) => { console.log('Received MCP request:', req.body);
src/server/index.test.ts+163 −0 modified@@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ +import supertest from 'supertest'; import { Client } from '../client/index.js'; import { InMemoryTransport } from '../inMemory.js'; import type { Transport } from '../shared/transport.js'; +import { createMcpExpressApp } from './index.js'; import { CreateMessageRequestSchema, CreateMessageResultSchema, @@ -2057,6 +2059,167 @@ test('should respect log level for transport with sessionId', async () => { expect(clientTransport.onmessage).toHaveBeenCalled(); }); +describe('createMcpExpressApp', () => { + test('should create an Express app', () => { + const app = createMcpExpressApp(); + expect(app).toBeDefined(); + }); + + test('should parse JSON bodies', async () => { + const app = createMcpExpressApp({ host: '0.0.0.0' }); // Disable host validation for this test + app.post('/test', (req, res) => { + res.json({ received: req.body }); + }); + + const response = await supertest(app).post('/test').send({ hello: 'world' }).set('Content-Type', 'application/json'); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ received: { hello: 'world' } }); + }); + + test('should reject requests with invalid Host header by default', async () => { + const app = createMcpExpressApp(); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + const response = await supertest(app).post('/test').set('Host', 'evil.com:3000').send({}); + + expect(response.status).toBe(403); + expect(response.body).toEqual({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Invalid Host: evil.com' + }, + id: null + }); + }); + + test('should allow requests with localhost Host header', async () => { + const app = createMcpExpressApp(); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + const response = await supertest(app).post('/test').set('Host', 'localhost:3000').send({}); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ success: true }); + }); + + test('should allow requests with 127.0.0.1 Host header', async () => { + const app = createMcpExpressApp(); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + const response = await supertest(app).post('/test').set('Host', '127.0.0.1:3000').send({}); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ success: true }); + }); + + test('should not apply host validation when host is 0.0.0.0', async () => { + const app = createMcpExpressApp({ host: '0.0.0.0' }); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + // Should allow any host when bound to 0.0.0.0 + const response = await supertest(app).post('/test').set('Host', 'any-host.com:3000').send({}); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ success: true }); + }); + + test('should apply host validation when host is explicitly localhost', async () => { + const app = createMcpExpressApp({ host: 'localhost' }); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + // Should reject non-localhost hosts + const response = await supertest(app).post('/test').set('Host', 'evil.com:3000').send({}); + + expect(response.status).toBe(403); + }); + + test('should allow requests with IPv6 localhost Host header', async () => { + const app = createMcpExpressApp(); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + const response = await supertest(app).post('/test').set('Host', '[::1]:3000').send({}); + + expect(response.status).toBe(200); + expect(response.body).toEqual({ success: true }); + }); + + test('should apply host validation when host is ::1 (IPv6 localhost)', async () => { + const app = createMcpExpressApp({ host: '::1' }); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + // Should reject non-localhost hosts + const response = await supertest(app).post('/test').set('Host', 'evil.com:3000').send({}); + + expect(response.status).toBe(403); + }); + + test('should warn when binding to 0.0.0.0', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + createMcpExpressApp({ host: '0.0.0.0' }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('0.0.0.0')); + warnSpy.mockRestore(); + }); + + test('should warn when binding to :: (IPv6 all interfaces)', () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + createMcpExpressApp({ host: '::' }); + expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('::')); + warnSpy.mockRestore(); + }); + + test('should use custom allowedHosts when provided', async () => { + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + // Should not warn when allowedHosts is provided + expect(warnSpy).not.toHaveBeenCalled(); + warnSpy.mockRestore(); + + // Should allow myapp.local + const allowedResponse = await supertest(app).post('/test').set('Host', 'myapp.local:3000').send({}); + expect(allowedResponse.status).toBe(200); + + // Should reject other hosts + const rejectedResponse = await supertest(app).post('/test').set('Host', 'evil.com:3000').send({}); + expect(rejectedResponse.status).toBe(403); + }); + + test('should override default localhost validation when allowedHosts is provided', async () => { + // Even though host is localhost, we're using custom allowedHosts + const app = createMcpExpressApp({ host: 'localhost', allowedHosts: ['custom.local'] }); + app.post('/test', (_req, res) => { + res.json({ success: true }); + }); + + // Should reject localhost since it's not in allowedHosts + const response = await supertest(app).post('/test').set('Host', 'localhost:3000').send({}); + expect(response.status).toBe(403); + + // Should allow custom.local + const allowedResponse = await supertest(app).post('/test').set('Host', 'custom.local:3000').send({}); + expect(allowedResponse.status).toBe(200); + }); +}); + describe('Task-based execution', () => { test('server with TaskStore should handle task-based tool execution', async () => { const taskStore = new InMemoryTaskStore();
src/server/index.ts+74 −0 modified@@ -1,4 +1,6 @@ +import express, { Express } from 'express'; import { mergeCapabilities, Protocol, type NotificationOptions, type ProtocolOptions, type RequestOptions } from '../shared/protocol.js'; +import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; import { type ClientCapabilities, type CreateMessageRequest, @@ -667,3 +669,75 @@ export class Server< return this.notification({ method: 'notifications/prompts/list_changed' }); } } + +/** + * Options for creating an MCP Express application. + */ +export interface CreateMcpExpressAppOptions { + /** + * The hostname to bind to. Defaults to '127.0.0.1'. + * When set to '127.0.0.1', 'localhost', or '::1', DNS rebinding protection is automatically enabled. + */ + host?: string; + + /** + * List of allowed hostnames for DNS rebinding protection. + * If provided, host header validation will be applied using this list. + * For IPv6, provide addresses with brackets (e.g., '[::1]'). + * + * This is useful when binding to '0.0.0.0' or '::' but still wanting + * to restrict which hostnames are allowed. + */ + allowedHosts?: string[]; +} + +/** + * Creates an Express application pre-configured for MCP servers. + * + * When the host is '127.0.0.1', 'localhost', or '::1' (the default is '127.0.0.1'), + * DNS rebinding protection middleware is automatically applied to protect against + * DNS rebinding attacks on localhost servers. + * + * @param options - Configuration options + * @returns A configured Express application + * + * @example + * ```typescript + * // Basic usage - defaults to 127.0.0.1 with DNS rebinding protection + * const app = createMcpExpressApp(); + * + * // Custom host - DNS rebinding protection only applied for localhost hosts + * const app = createMcpExpressApp({ host: '0.0.0.0' }); // No automatic DNS rebinding protection + * const app = createMcpExpressApp({ host: 'localhost' }); // DNS rebinding protection enabled + * + * // Custom allowed hosts for non-localhost binding + * const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local', 'localhost'] }); + * ``` + */ +export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { + const { host = '127.0.0.1', allowedHosts } = options; + + const app = express(); + app.use(express.json()); + + // If allowedHosts is explicitly provided, use that for validation + if (allowedHosts) { + app.use(hostHeaderValidation(allowedHosts)); + } else { + // Apply DNS rebinding protection automatically for localhost hosts + const localhostHosts = ['127.0.0.1', 'localhost', '::1']; + if (localhostHosts.includes(host)) { + app.use(localhostHostValidation()); + } else if (host === '0.0.0.0' || host === '::') { + // Warn when binding to all interfaces without DNS rebinding protection + // eslint-disable-next-line no-console + console.warn( + `Warning: Server is binding to ${host} without DNS rebinding protection. ` + + 'Consider using the allowedHosts option to restrict allowed hosts, ' + + 'or use authentication to protect your server.' + ); + } + } + + return app; +}
src/server/middleware/hostHeaderValidation.ts+79 −0 added@@ -0,0 +1,79 @@ +import { Request, Response, NextFunction, RequestHandler } from 'express'; + +/** + * Express middleware for DNS rebinding protection. + * Validates Host header hostname (port-agnostic) against an allowed list. + * + * This is particularly important for servers without authorization or HTTPS, + * such as localhost servers or development servers. DNS rebinding attacks can + * bypass same-origin policy by manipulating DNS to point a domain to a + * localhost address, allowing malicious websites to access your local server. + * + * @param allowedHostnames - List of allowed hostnames (without ports). + * For IPv6, provide the address with brackets (e.g., '[::1]'). + * @returns Express middleware function + * + * @example + * ```typescript + * const middleware = hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); + * app.use(middleware); + * ``` + */ +export function hostHeaderValidation(allowedHostnames: string[]): RequestHandler { + return (req: Request, res: Response, next: NextFunction) => { + const hostHeader = req.headers.host; + if (!hostHeader) { + res.status(403).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: 'Missing Host header' + }, + id: null + }); + return; + } + + // Use URL API to parse hostname (handles IPv4, IPv6, and regular hostnames) + let hostname: string; + try { + hostname = new URL(`http://${hostHeader}`).hostname; + } catch { + res.status(403).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: `Invalid Host header: ${hostHeader}` + }, + id: null + }); + return; + } + + if (!allowedHostnames.includes(hostname)) { + res.status(403).json({ + jsonrpc: '2.0', + error: { + code: -32000, + message: `Invalid Host: ${hostname}` + }, + id: null + }); + return; + } + next(); + }; +} + +/** + * Convenience middleware for localhost DNS rebinding protection. + * Allows only localhost, 127.0.0.1, and [::1] (IPv6 localhost) hostnames. + * + * @example + * ```typescript + * app.use(localhostHostValidation()); + * ``` + */ +export function localhostHostValidation(): RequestHandler { + return hostHeaderValidation(['localhost', '127.0.0.1', '[::1]']); +}
src/server/sse.ts+6 −0 modified@@ -16,18 +16,24 @@ export interface SSEServerTransportOptions { /** * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ allowedHosts?: string[]; /** * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ allowedOrigins?: string[]; /** * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean; }
src/server/streamableHttp.ts+6 −0 modified@@ -104,18 +104,24 @@ export interface StreamableHTTPServerTransportOptions { /** * List of allowed host header values for DNS rebinding protection. * If not specified, host validation is disabled. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ allowedHosts?: string[]; /** * List of allowed origin header values for DNS rebinding protection. * If not specified, origin validation is disabled. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ allowedOrigins?: string[]; /** * Enable DNS rebinding protection (requires allowedHosts and/or allowedOrigins to be configured). * Default is false for backwards compatibility. + * @deprecated Use the `hostHeaderValidation` middleware from `@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js` instead, + * or use `createMcpExpressApp` from `@modelcontextprotocol/sdk/server/index.js` which includes localhost protection by default. */ enableDnsRebindingProtection?: boolean;
608360047dc6Modify Origin header validation in validateRequestHeaders (streamableHttp.ts and sse.ts) to allow requests without an Origin, as they are not relevant to server DNS rebinding protection. (#1205)
4 files changed · +46 −2
src/server/sse.test.ts+21 −0 modified@@ -547,6 +547,27 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); }); + it('should accept requests without origin headers', async () => { + const mockRes = createMockResponse(); + const transport = new SSEServerTransport('/messages', mockRes, { + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + await transport.start(); + + const mockReq = createMockRequest({ + headers: { + 'content-type': 'application/json' + } + }); + const mockHandleRes = createMockResponse(); + + await transport.handlePostMessage(mockReq, mockHandleRes, { jsonrpc: '2.0', method: 'test' }); + + expect(mockHandleRes.writeHead).toHaveBeenCalledWith(202); + expect(mockHandleRes.end).toHaveBeenCalledWith('Accepted'); + }); + it('should reject requests with disallowed origin headers', async () => { const mockRes = createMockResponse(); const transport = new SSEServerTransport('/messages', mockRes, {
src/server/sse.ts+1 −1 modified@@ -79,7 +79,7 @@ export class SSEServerTransport implements Transport { // Validate Origin header if allowedOrigins is configured if (this._options.allowedOrigins && this._options.allowedOrigins.length > 0) { const originHeader = req.headers.origin; - if (!originHeader || !this._options.allowedOrigins.includes(originHeader)) { + if (originHeader && !this._options.allowedOrigins.includes(originHeader)) { return `Invalid Origin header: ${originHeader}`; } }
src/server/streamableHttp.test.ts+23 −0 modified@@ -2678,6 +2678,29 @@ describe.each(zodTestMatrix)('$zodVersionLabel', (entry: ZodMatrixEntry) => { const body = await response.json(); expect(body.error.message).toBe('Invalid Origin header: http://evil.com'); }); + + it('should accept requests without origin headers', async () => { + const result = await createTestServerWithDnsProtection({ + sessionIdGenerator: undefined, + allowedOrigins: ['http://localhost:3000', 'https://example.com'], + enableDnsRebindingProtection: true + }); + server = result.server; + transport = result.transport; + baseUrl = result.baseUrl; + + const response = await fetch(baseUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json, text/event-stream' + }, + body: JSON.stringify(TEST_MESSAGES.initialize) + }); + + // Should pass even with no Origin headers because requests that do not come from browsers may not have Origin and DNS rebinding attacks can only be performed via browsers + expect(response.status).toBe(200); + }); }); describe('enableDnsRebindingProtection option', () => {
src/server/streamableHttp.ts+1 −1 modified@@ -228,7 +228,7 @@ export class StreamableHTTPServerTransport implements Transport { // Validate Origin header if allowedOrigins is configured if (this._allowedOrigins && this._allowedOrigins.length > 0) { const originHeader = req.headers.origin; - if (!originHeader || !this._allowedOrigins.includes(originHeader)) { + if (originHeader && !this._allowedOrigins.includes(originHeader)) { return `Invalid Origin header: ${originHeader}`; } }
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-w48q-cv73-mx4wghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-66414ghsaADVISORY
- github.com/modelcontextprotocol/typescript-sdk/commit/09623e2aa5044f9e9da62c73d820a8250b9d97edghsax_refsource_MISCWEB
- github.com/modelcontextprotocol/typescript-sdk/commit/608360047dc6899f1cf4f0226eb62fe7b11b3898ghsaWEB
- github.com/modelcontextprotocol/typescript-sdk/pull/1205ghsaWEB
- github.com/modelcontextprotocol/typescript-sdk/security/advisories/GHSA-w48q-cv73-mx4wghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.