From d75a4141e4952ed23da61e6963fde6c13b7d599c Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 27 Mar 2026 00:23:00 +0200 Subject: [PATCH 1/2] add skipostHeaderValidation option, default false, remove console warning --- packages/middleware/express/src/express.ts | 73 +++++++++++-------- .../middleware/express/test/express.test.ts | 46 +++--------- packages/middleware/hono/src/hono.ts | 73 +++++++++++-------- packages/middleware/hono/test/hono.test.ts | 24 ++++-- 4 files changed, 117 insertions(+), 99 deletions(-) diff --git a/packages/middleware/express/src/express.ts b/packages/middleware/express/src/express.ts index 252502952..4b0a24a47 100644 --- a/packages/middleware/express/src/express.ts +++ b/packages/middleware/express/src/express.ts @@ -3,26 +3,47 @@ import express from 'express'; import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; +/** + * Host header validation options for DNS rebinding protection. + * + * Either skip validation entirely, or optionally provide an explicit allowlist. + */ +export type HostHeaderValidationOptions = + | { + /** + * When set to `true`, disables all automatic host header validation + * (DNS rebinding protection). + * + * Use this when the server sits behind a reverse proxy or load balancer + * that rewrites the `Host` header, or when running in an isolated network + * (e.g., containers) where DNS rebinding is not a concern. + */ + skipHostHeaderValidation: true; + allowedHosts?: never; + } + | { + skipHostHeaderValidation?: false; + /** + * 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[]; + }; + /** * Options for creating an MCP Express application. */ -export interface CreateMcpExpressAppOptions { +export type 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[]; - /** * Controls the maximum request body size for the JSON body parser. * Passed directly to Express's `express.json({ limit })` option. @@ -31,7 +52,7 @@ export interface CreateMcpExpressAppOptions { * @example '1mb', '500kb', '10mb' */ jsonLimit?: string; -} +} & HostHeaderValidationOptions; /** * Creates an Express application pre-configured for MCP servers. @@ -60,27 +81,21 @@ export interface CreateMcpExpressAppOptions { * ``` */ export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express { - const { host = '127.0.0.1', allowedHosts, jsonLimit } = options; + const { host = '127.0.0.1', allowedHosts, jsonLimit, skipHostHeaderValidation } = options; const app = express(); app.use(express.json(jsonLimit ? { limit: jsonLimit } : undefined)); - // 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.' - ); + if (!skipHostHeaderValidation) { + // 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()); + } } } diff --git a/packages/middleware/express/test/express.test.ts b/packages/middleware/express/test/express.test.ts index f4be9f998..3829a094d 100644 --- a/packages/middleware/express/test/express.test.ts +++ b/packages/middleware/express/test/express.test.ts @@ -128,55 +128,29 @@ describe('@modelcontextprotocol/express', () => { }); test('should use allowedHosts when provided', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); - warn.mockRestore(); expect(app).toBeDefined(); }); - test('should warn when binding to 0.0.0.0 without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpExpressApp({ host: '0.0.0.0' }); - - expect(warn).toHaveBeenCalledWith( - expect.stringContaining('Warning: Server is binding to 0.0.0.0 without DNS rebinding protection') - ); - - warn.mockRestore(); - }); - - test('should warn when binding to :: without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpExpressApp({ host: '::' }); - - expect(warn).toHaveBeenCalledWith(expect.stringContaining('Warning: Server is binding to :: without DNS rebinding protection')); + test('should not apply host validation for non-localhost hosts without allowedHosts', () => { + // For arbitrary hosts (not 0.0.0.0 or ::), no validation is applied + const app = createMcpExpressApp({ host: '192.168.1.1' }); - warn.mockRestore(); + expect(app).toBeDefined(); }); - test('should not warn for 0.0.0.0 when allowedHosts is provided', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); + test('should skip host header validation when skipHostHeaderValidation is true', () => { + const app = createMcpExpressApp({ host: '127.0.0.1', skipHostHeaderValidation: true }); - expect(warn).not.toHaveBeenCalled(); - - warn.mockRestore(); + expect(app).toBeDefined(); + // Localhost validation would normally be applied, but skipHostHeaderValidation disables it }); - test('should not apply host validation for non-localhost hosts without allowedHosts', () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); - - // For arbitrary hosts (not 0.0.0.0 or ::), no validation is applied and no warning - const app = createMcpExpressApp({ host: '192.168.1.1' }); + test('should skip host header validation for 0.0.0.0 when skipHostHeaderValidation is true', () => { + const app = createMcpExpressApp({ host: '0.0.0.0', skipHostHeaderValidation: true }); - expect(warn).not.toHaveBeenCalled(); expect(app).toBeDefined(); - - warn.mockRestore(); }); test('should accept jsonLimit option', () => { diff --git a/packages/middleware/hono/src/hono.ts b/packages/middleware/hono/src/hono.ts index eda3e5d8f..ee8b8d272 100644 --- a/packages/middleware/hono/src/hono.ts +++ b/packages/middleware/hono/src/hono.ts @@ -3,26 +3,47 @@ import { Hono } from 'hono'; import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js'; +/** + * Host header validation options for DNS rebinding protection. + * + * Either skip validation entirely, or optionally provide an explicit allowlist. + */ +export type HostHeaderValidationOptions = + | { + /** + * When set to `true`, disables all automatic host header validation + * (DNS rebinding protection). + * + * Use this when the server sits behind a reverse proxy or load balancer + * that rewrites the `Host` header, or when running in an isolated network + * (e.g., containers) where DNS rebinding is not a concern. + */ + skipHostHeaderValidation: true; + allowedHosts?: never; + } + | { + skipHostHeaderValidation?: false; + /** + * 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[]; + }; + /** * Options for creating an MCP Hono application. */ -export interface CreateMcpHonoAppOptions { +export type CreateMcpHonoAppOptions = { /** * 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[]; -} +} & HostHeaderValidationOptions; /** * Creates a Hono application pre-configured for MCP servers. @@ -39,7 +60,7 @@ export interface CreateMcpHonoAppOptions { * @returns A configured Hono application */ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { - const { host = '127.0.0.1', allowedHosts } = options; + const { host = '127.0.0.1', allowedHosts, skipHostHeaderValidation } = options; const app = new Hono(); @@ -67,22 +88,16 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono { return await next(); }); - // 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.' - ); + if (!skipHostHeaderValidation) { + // 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()); + } } } diff --git a/packages/middleware/hono/test/hono.test.ts b/packages/middleware/hono/test/hono.test.ts index a080f1ffb..6612b7bb2 100644 --- a/packages/middleware/hono/test/hono.test.ts +++ b/packages/middleware/hono/test/hono.test.ts @@ -1,6 +1,5 @@ import type { Context } from 'hono'; import { Hono } from 'hono'; -import { vi } from 'vitest'; import { createMcpHonoApp } from '../src/hono.js'; import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js'; @@ -40,9 +39,7 @@ describe('@modelcontextprotocol/hono', () => { }); test('createMcpHonoApp uses allowedHosts when provided (even when binding to 0.0.0.0)', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpHonoApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] }); - warn.mockRestore(); app.get('/health', c => c.text('ok')); @@ -54,9 +51,7 @@ describe('@modelcontextprotocol/hono', () => { }); test('createMcpHonoApp does not apply host validation for 0.0.0.0 without allowedHosts', async () => { - const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); const app = createMcpHonoApp({ host: '0.0.0.0' }); - warn.mockRestore(); app.get('/health', c => c.text('ok')); @@ -64,6 +59,25 @@ describe('@modelcontextprotocol/hono', () => { expect(res.status).toBe(200); }); + test('createMcpHonoApp skips all host validation when skipHostHeaderValidation is true', async () => { + const app = createMcpHonoApp({ host: '127.0.0.1', skipHostHeaderValidation: true }); + + app.get('/health', c => c.text('ok')); + + // Would normally be blocked by localhost validation, but skipHostHeaderValidation disables it + const res = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } }); + expect(res.status).toBe(200); + }); + + test('createMcpHonoApp skips validation for 0.0.0.0 when skipHostHeaderValidation is true', async () => { + const app = createMcpHonoApp({ host: '0.0.0.0', skipHostHeaderValidation: true }); + + app.get('/health', c => c.text('ok')); + + const res = await app.request('http://localhost/health', { headers: { Host: 'anything.com:3000' } }); + expect(res.status).toBe(200); + }); + test('createMcpHonoApp parses JSON bodies into parsedBody (express.json()-like)', async () => { const app = createMcpHonoApp(); app.post('/echo', (c: Context) => c.json(c.get('parsedBody'))); From d9202053e8cf16088c72a44b199e9b027c06010a Mon Sep 17 00:00:00 2001 From: Konstantin Konstantinov Date: Fri, 27 Mar 2026 00:41:32 +0200 Subject: [PATCH 2/2] test fixes --- .../middleware/express/test/express.test.ts | 4 +-- test/integration/test/server.test.ts | 33 ++++++++++--------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/packages/middleware/express/test/express.test.ts b/packages/middleware/express/test/express.test.ts index 3829a094d..9c8d1d040 100644 --- a/packages/middleware/express/test/express.test.ts +++ b/packages/middleware/express/test/express.test.ts @@ -141,15 +141,13 @@ describe('@modelcontextprotocol/express', () => { }); test('should skip host header validation when skipHostHeaderValidation is true', () => { + // HTTP-level verification is in integration tests (test/integration/test/server.test.ts) const app = createMcpExpressApp({ host: '127.0.0.1', skipHostHeaderValidation: true }); - expect(app).toBeDefined(); - // Localhost validation would normally be applied, but skipHostHeaderValidation disables it }); test('should skip host header validation for 0.0.0.0 when skipHostHeaderValidation is true', () => { const app = createMcpExpressApp({ host: '0.0.0.0', skipHostHeaderValidation: true }); - expect(app).toBeDefined(); }); diff --git a/test/integration/test/server.test.ts b/test/integration/test/server.test.ts index 825af7ea4..f93b6cf40 100644 --- a/test/integration/test/server.test.ts +++ b/test/integration/test/server.test.ts @@ -2308,31 +2308,34 @@ describe('createMcpExpressApp', () => { 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 not apply host validation for 0.0.0.0 without allowedHosts', async () => { + const app = createMcpExpressApp({ host: '0.0.0.0' }); + app.post('/test', (_req: Request, res: Response) => { + res.json({ success: true }); + }); + + // No host validation applied, so any host should be accepted + const response = await supertest(app).post('/test').set('Host', 'anything.com:3000').send({}); + expect(response.status).toBe(200); }); - 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 skip host validation when skipHostHeaderValidation is true', async () => { + const app = createMcpExpressApp({ host: '127.0.0.1', skipHostHeaderValidation: true }); + app.post('/test', (_req: Request, res: Response) => { + res.json({ success: true }); + }); + + // Localhost validation would normally block this, but skipHostHeaderValidation disables it + const response = await supertest(app).post('/test').set('Host', 'evil.com:3000').send({}); + expect(response.status).toBe(200); }); 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: Request, res: Response) => { 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);