Skip to content

Commit d75a414

Browse files
committed
add skipostHeaderValidation option, default false, remove console warning
1 parent 379392d commit d75a414

4 files changed

Lines changed: 117 additions & 99 deletions

File tree

packages/middleware/express/src/express.ts

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,47 @@ import express from 'express';
33

44
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
55

6+
/**
7+
* Host header validation options for DNS rebinding protection.
8+
*
9+
* Either skip validation entirely, or optionally provide an explicit allowlist.
10+
*/
11+
export type HostHeaderValidationOptions =
12+
| {
13+
/**
14+
* When set to `true`, disables all automatic host header validation
15+
* (DNS rebinding protection).
16+
*
17+
* Use this when the server sits behind a reverse proxy or load balancer
18+
* that rewrites the `Host` header, or when running in an isolated network
19+
* (e.g., containers) where DNS rebinding is not a concern.
20+
*/
21+
skipHostHeaderValidation: true;
22+
allowedHosts?: never;
23+
}
24+
| {
25+
skipHostHeaderValidation?: false;
26+
/**
27+
* List of allowed hostnames for DNS rebinding protection.
28+
* If provided, host header validation will be applied using this list.
29+
* For IPv6, provide addresses with brackets (e.g., `'[::1]'`).
30+
*
31+
* This is useful when binding to `'0.0.0.0'` or `'::'` but still wanting
32+
* to restrict which hostnames are allowed.
33+
*/
34+
allowedHosts?: string[];
35+
};
36+
637
/**
738
* Options for creating an MCP Express application.
839
*/
9-
export interface CreateMcpExpressAppOptions {
40+
export type CreateMcpExpressAppOptions = {
1041
/**
1142
* The hostname to bind to. Defaults to `'127.0.0.1'`.
1243
* When set to `'127.0.0.1'`, `'localhost'`, or `'::1'`, DNS rebinding protection is automatically enabled.
1344
*/
1445
host?: string;
1546

16-
/**
17-
* List of allowed hostnames for DNS rebinding protection.
18-
* If provided, host header validation will be applied using this list.
19-
* For IPv6, provide addresses with brackets (e.g., `'[::1]'`).
20-
*
21-
* This is useful when binding to `'0.0.0.0'` or `'::'` but still wanting
22-
* to restrict which hostnames are allowed.
23-
*/
24-
allowedHosts?: string[];
25-
2647
/**
2748
* Controls the maximum request body size for the JSON body parser.
2849
* Passed directly to Express's `express.json({ limit })` option.
@@ -31,7 +52,7 @@ export interface CreateMcpExpressAppOptions {
3152
* @example '1mb', '500kb', '10mb'
3253
*/
3354
jsonLimit?: string;
34-
}
55+
} & HostHeaderValidationOptions;
3556

3657
/**
3758
* Creates an Express application pre-configured for MCP servers.
@@ -60,27 +81,21 @@ export interface CreateMcpExpressAppOptions {
6081
* ```
6182
*/
6283
export function createMcpExpressApp(options: CreateMcpExpressAppOptions = {}): Express {
63-
const { host = '127.0.0.1', allowedHosts, jsonLimit } = options;
84+
const { host = '127.0.0.1', allowedHosts, jsonLimit, skipHostHeaderValidation } = options;
6485

6586
const app = express();
6687
app.use(express.json(jsonLimit ? { limit: jsonLimit } : undefined));
6788

68-
// If allowedHosts is explicitly provided, use that for validation
69-
if (allowedHosts) {
70-
app.use(hostHeaderValidation(allowedHosts));
71-
} else {
72-
// Apply DNS rebinding protection automatically for localhost hosts
73-
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
74-
if (localhostHosts.includes(host)) {
75-
app.use(localhostHostValidation());
76-
} else if (host === '0.0.0.0' || host === '::') {
77-
// Warn when binding to all interfaces without DNS rebinding protection
78-
// eslint-disable-next-line no-console
79-
console.warn(
80-
`Warning: Server is binding to ${host} without DNS rebinding protection. ` +
81-
'Consider using the allowedHosts option to restrict allowed hosts, ' +
82-
'or use authentication to protect your server.'
83-
);
89+
if (!skipHostHeaderValidation) {
90+
// If allowedHosts is explicitly provided, use that for validation
91+
if (allowedHosts) {
92+
app.use(hostHeaderValidation(allowedHosts));
93+
} else {
94+
// Apply DNS rebinding protection automatically for localhost hosts
95+
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
96+
if (localhostHosts.includes(host)) {
97+
app.use(localhostHostValidation());
98+
}
8499
}
85100
}
86101

packages/middleware/express/test/express.test.ts

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -128,55 +128,29 @@ describe('@modelcontextprotocol/express', () => {
128128
});
129129

130130
test('should use allowedHosts when provided', () => {
131-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
132131
const app = createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] });
133-
warn.mockRestore();
134132

135133
expect(app).toBeDefined();
136134
});
137135

138-
test('should warn when binding to 0.0.0.0 without allowedHosts', () => {
139-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
140-
141-
createMcpExpressApp({ host: '0.0.0.0' });
142-
143-
expect(warn).toHaveBeenCalledWith(
144-
expect.stringContaining('Warning: Server is binding to 0.0.0.0 without DNS rebinding protection')
145-
);
146-
147-
warn.mockRestore();
148-
});
149-
150-
test('should warn when binding to :: without allowedHosts', () => {
151-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
152-
153-
createMcpExpressApp({ host: '::' });
154-
155-
expect(warn).toHaveBeenCalledWith(expect.stringContaining('Warning: Server is binding to :: without DNS rebinding protection'));
136+
test('should not apply host validation for non-localhost hosts without allowedHosts', () => {
137+
// For arbitrary hosts (not 0.0.0.0 or ::), no validation is applied
138+
const app = createMcpExpressApp({ host: '192.168.1.1' });
156139

157-
warn.mockRestore();
140+
expect(app).toBeDefined();
158141
});
159142

160-
test('should not warn for 0.0.0.0 when allowedHosts is provided', () => {
161-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
162-
163-
createMcpExpressApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] });
143+
test('should skip host header validation when skipHostHeaderValidation is true', () => {
144+
const app = createMcpExpressApp({ host: '127.0.0.1', skipHostHeaderValidation: true });
164145

165-
expect(warn).not.toHaveBeenCalled();
166-
167-
warn.mockRestore();
146+
expect(app).toBeDefined();
147+
// Localhost validation would normally be applied, but skipHostHeaderValidation disables it
168148
});
169149

170-
test('should not apply host validation for non-localhost hosts without allowedHosts', () => {
171-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
172-
173-
// For arbitrary hosts (not 0.0.0.0 or ::), no validation is applied and no warning
174-
const app = createMcpExpressApp({ host: '192.168.1.1' });
150+
test('should skip host header validation for 0.0.0.0 when skipHostHeaderValidation is true', () => {
151+
const app = createMcpExpressApp({ host: '0.0.0.0', skipHostHeaderValidation: true });
175152

176-
expect(warn).not.toHaveBeenCalled();
177153
expect(app).toBeDefined();
178-
179-
warn.mockRestore();
180154
});
181155

182156
test('should accept jsonLimit option', () => {

packages/middleware/hono/src/hono.ts

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,47 @@ import { Hono } from 'hono';
33

44
import { hostHeaderValidation, localhostHostValidation } from './middleware/hostHeaderValidation.js';
55

6+
/**
7+
* Host header validation options for DNS rebinding protection.
8+
*
9+
* Either skip validation entirely, or optionally provide an explicit allowlist.
10+
*/
11+
export type HostHeaderValidationOptions =
12+
| {
13+
/**
14+
* When set to `true`, disables all automatic host header validation
15+
* (DNS rebinding protection).
16+
*
17+
* Use this when the server sits behind a reverse proxy or load balancer
18+
* that rewrites the `Host` header, or when running in an isolated network
19+
* (e.g., containers) where DNS rebinding is not a concern.
20+
*/
21+
skipHostHeaderValidation: true;
22+
allowedHosts?: never;
23+
}
24+
| {
25+
skipHostHeaderValidation?: false;
26+
/**
27+
* List of allowed hostnames for DNS rebinding protection.
28+
* If provided, host header validation will be applied using this list.
29+
* For IPv6, provide addresses with brackets (e.g., '[::1]').
30+
*
31+
* This is useful when binding to '0.0.0.0' or '::' but still wanting
32+
* to restrict which hostnames are allowed.
33+
*/
34+
allowedHosts?: string[];
35+
};
36+
637
/**
738
* Options for creating an MCP Hono application.
839
*/
9-
export interface CreateMcpHonoAppOptions {
40+
export type CreateMcpHonoAppOptions = {
1041
/**
1142
* The hostname to bind to. Defaults to `'127.0.0.1'`.
1243
* When set to `'127.0.0.1'`, `'localhost'`, or `'::1'`, DNS rebinding protection is automatically enabled.
1344
*/
1445
host?: string;
15-
16-
/**
17-
* List of allowed hostnames for DNS rebinding protection.
18-
* If provided, host header validation will be applied using this list.
19-
* For IPv6, provide addresses with brackets (e.g., '[::1]').
20-
*
21-
* This is useful when binding to '0.0.0.0' or '::' but still wanting
22-
* to restrict which hostnames are allowed.
23-
*/
24-
allowedHosts?: string[];
25-
}
46+
} & HostHeaderValidationOptions;
2647

2748
/**
2849
* Creates a Hono application pre-configured for MCP servers.
@@ -39,7 +60,7 @@ export interface CreateMcpHonoAppOptions {
3960
* @returns A configured Hono application
4061
*/
4162
export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
42-
const { host = '127.0.0.1', allowedHosts } = options;
63+
const { host = '127.0.0.1', allowedHosts, skipHostHeaderValidation } = options;
4364

4465
const app = new Hono();
4566

@@ -67,22 +88,16 @@ export function createMcpHonoApp(options: CreateMcpHonoAppOptions = {}): Hono {
6788
return await next();
6889
});
6990

70-
// If allowedHosts is explicitly provided, use that for validation.
71-
if (allowedHosts) {
72-
app.use('*', hostHeaderValidation(allowedHosts));
73-
} else {
74-
// Apply DNS rebinding protection automatically for localhost hosts.
75-
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
76-
if (localhostHosts.includes(host)) {
77-
app.use('*', localhostHostValidation());
78-
} else if (host === '0.0.0.0' || host === '::') {
79-
// Warn when binding to all interfaces without DNS rebinding protection.
80-
// eslint-disable-next-line no-console
81-
console.warn(
82-
`Warning: Server is binding to ${host} without DNS rebinding protection. ` +
83-
'Consider using the allowedHosts option to restrict allowed hosts, ' +
84-
'or use authentication to protect your server.'
85-
);
91+
if (!skipHostHeaderValidation) {
92+
// If allowedHosts is explicitly provided, use that for validation.
93+
if (allowedHosts) {
94+
app.use('*', hostHeaderValidation(allowedHosts));
95+
} else {
96+
// Apply DNS rebinding protection automatically for localhost hosts.
97+
const localhostHosts = ['127.0.0.1', 'localhost', '::1'];
98+
if (localhostHosts.includes(host)) {
99+
app.use('*', localhostHostValidation());
100+
}
86101
}
87102
}
88103

packages/middleware/hono/test/hono.test.ts

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { Context } from 'hono';
22
import { Hono } from 'hono';
3-
import { vi } from 'vitest';
43

54
import { createMcpHonoApp } from '../src/hono.js';
65
import { hostHeaderValidation } from '../src/middleware/hostHeaderValidation.js';
@@ -40,9 +39,7 @@ describe('@modelcontextprotocol/hono', () => {
4039
});
4140

4241
test('createMcpHonoApp uses allowedHosts when provided (even when binding to 0.0.0.0)', async () => {
43-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
4442
const app = createMcpHonoApp({ host: '0.0.0.0', allowedHosts: ['myapp.local'] });
45-
warn.mockRestore();
4643

4744
app.get('/health', c => c.text('ok'));
4845

@@ -54,16 +51,33 @@ describe('@modelcontextprotocol/hono', () => {
5451
});
5552

5653
test('createMcpHonoApp does not apply host validation for 0.0.0.0 without allowedHosts', async () => {
57-
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
5854
const app = createMcpHonoApp({ host: '0.0.0.0' });
59-
warn.mockRestore();
6055

6156
app.get('/health', c => c.text('ok'));
6257

6358
const res = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } });
6459
expect(res.status).toBe(200);
6560
});
6661

62+
test('createMcpHonoApp skips all host validation when skipHostHeaderValidation is true', async () => {
63+
const app = createMcpHonoApp({ host: '127.0.0.1', skipHostHeaderValidation: true });
64+
65+
app.get('/health', c => c.text('ok'));
66+
67+
// Would normally be blocked by localhost validation, but skipHostHeaderValidation disables it
68+
const res = await app.request('http://localhost/health', { headers: { Host: 'evil.com:3000' } });
69+
expect(res.status).toBe(200);
70+
});
71+
72+
test('createMcpHonoApp skips validation for 0.0.0.0 when skipHostHeaderValidation is true', async () => {
73+
const app = createMcpHonoApp({ host: '0.0.0.0', skipHostHeaderValidation: true });
74+
75+
app.get('/health', c => c.text('ok'));
76+
77+
const res = await app.request('http://localhost/health', { headers: { Host: 'anything.com:3000' } });
78+
expect(res.status).toBe(200);
79+
});
80+
6781
test('createMcpHonoApp parses JSON bodies into parsedBody (express.json()-like)', async () => {
6882
const app = createMcpHonoApp();
6983
app.post('/echo', (c: Context) => c.json(c.get('parsedBody')));

0 commit comments

Comments
 (0)