Merge commit from fork
* Add secure default for DNS rebinding, use in examples * Update README examples to use createMcpExpressApp, bump to 1.23.0 - Update all server examples to use createMcpExpressApp() for secure defaults - Rewrite DNS Rebinding Protection section to document new middleware approach - Bump version from 1.23.0-beta.0 to 1.23.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Add IPv6 support, allowedHosts option, and warning for unprotected servers - Use native URL API to parse Host headers (handles IPv6 correctly) - Add [::1] (IPv6 localhost) to localhostHostValidation - Add ::1 to list of localhost hosts that get automatic protection - Add allowedHosts option to createMcpExpressApp for custom host validation - Warn when binding to 0.0.0.0 or :: without allowedHosts configured - Add tests for IPv6 and allowedHosts functionality 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update remaining examples to use createMcpExpressApp and add DNS rebinding docs - Update simpleTaskInteractive.ts, ssePollingExample.ts, and elicitationUrlExample.ts to use createMcpExpressApp() instead of express() + express.json() - Add DNS rebinding protection section to docs/server.md explaining how to use createMcpExpressApp() and hostHeaderValidation middleware 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cf51343cc1
commit
09623e2aa5
1 changed files with 387 additions and 77 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in a new issue