feat: add DNS rebinding protection conformance tests (#115)

* feat: add DNS rebinding protection conformance tests

Add a new server conformance scenario to verify that localhost MCP servers
properly validate Host headers to prevent DNS rebinding attacks.

Checks:
- localhost-host-rebinding-rejected: Verifies server returns 403 for
  non-localhost Host headers (e.g., evil.example.com)
- localhost-host-valid-accepted: Verifies server accepts requests with
  valid localhost Host headers

Also updates the everything-server example to use createMcpExpressApp()
which includes DNS rebinding protection by default.

Closes #103

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: add negative test case for DNS rebinding protection

Add a minimal MCP server that intentionally omits DNS rebinding protection
to serve as a negative test case. This server is expected to FAIL the
dns-rebinding-protection scenario.

Also update README to document the negative test case.

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: accept any 4xx status code for DNS rebinding rejection

Update the DNS rebinding protection test to accept any 4xx status code
as a valid rejection response, not just 403. This accommodates different
HTTP semantics:
- 403 Forbidden: Per MCP spec
- 421 Misdirected Request: RFC 7540 (semantically correct for DNS rebinding)
- Other 4xx: Implementation-specific error codes

* refactor: address PR review comments

- Simplify negative test server (remove event store, sessions)
- Update spec reference URL to security_best_practices
- Clarify scope: localhost servers without HTTPS/auth
- DRY up checks.push using spread operator
- Fix valid host check to require 2xx response

* fix: include error details in catch block

* refactor: simplify negative test server to stateless

* refactor: check both Host and Origin headers for DNS rebinding protection

- Send both Host and Origin headers in test requests so servers checking
  either header will pass the conformance test
- Add second spec reference for Origin check (transports#security-warning)
- Update descriptions to mention both Host and Origin headers
- Fix protocol version to 2025-11-25
- Remove unnecessary 'as const' type assertions

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Paul Carleton 2026-01-22 18:37:48 +00:00 • committed by GitHub
commit d0b2b4914d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 340 additions and 4 deletions

View file

@ -170,3 +170,21 @@ If you're implementing MCP in another language/SDK:
5. **Handle Notifications Carefully**: Catch/ignore errors when no client is connected
**Goal**: All SDK example servers provide the same interface, enabling a single test suite to verify conformance across all implementations.
## Negative Test Cases
### no-dns-rebinding-protection.ts
A minimal MCP server that intentionally omits DNS rebinding protection. This is a **negative test case** that demonstrates what a vulnerable server looks like and is expected to **FAIL** the `dns-rebinding-protection` conformance scenario.
```bash
# Run the vulnerable server
npx tsx no-dns-rebinding-protection.ts
# This should FAIL the dns-rebinding-protection checks
npx @modelcontextprotocol/conformance server \
--url http://localhost:3003/mcp \
--scenario dns-rebinding-protection
```
**DO NOT** use this pattern in production servers. Always use `createMcpExpressApp()` or the `localhostHostValidation()` middleware for localhost servers.

View file

@ -18,6 +18,7 @@ import {
EventId,
StreamId
} from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { createMcpExpressApp } from '@modelcontextprotocol/sdk/server/express.js';
import {
ElicitResultSchema,
ListToolsRequestSchema,
@ -26,7 +27,6 @@ import {
} from '@modelcontextprotocol/sdk/types.js';
import { zodToJsonSchema } from 'zod-to-json-schema';
import { z } from 'zod';
import express from 'express';
import cors from 'cors';
import { randomUUID } from 'crypto';
@ -1055,8 +1055,8 @@ function isInitializeRequest(body: any): boolean {
// ===== EXPRESS APP =====
const app = express();
app.use(express.json());
// Use createMcpExpressApp for DNS rebinding protection on localhost
const app = createMcpExpressApp();
// Configure CORS to expose Mcp-Session-Id header for browser-based clients
app.use(

View file

@ -0,0 +1,64 @@
#!/usr/bin/env node
/**
* MCP Server WITHOUT DNS Rebinding Protection - Negative Test Case
*
* This is the simplest possible vulnerable server to demonstrate what happens
* when DNS rebinding protection is omitted. DO NOT use this pattern in production.
*
* This server should FAIL the dns-rebinding-protection conformance scenario.
*/
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
// Create minimal MCP server
const server = new McpServer({
name: 'no-dns-rebinding-protection-server',
version: '1.0.0'
});
// Add a simple tool
server.tool(
'echo',
'Echo the input back',
{ message: { type: 'string' } },
async ({ message }) => ({
content: [{ type: 'text', text: `Echo: ${message}` }]
})
);
// === VULNERABLE EXPRESS APP ===
// This intentionally does NOT use createMcpExpressApp() or localhostHostValidation()
const app = express();
app.use(express.json());
// NO DNS rebinding protection middleware here!
app.post('/mcp', async (req, res) => {
try {
// Stateless: no session ID
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
},
id: null
});
}
}
});
const PORT = parseInt(process.env.PORT || '3003', 10);
app.listen(PORT, '127.0.0.1', () => {
console.log(`Vulnerable server running on http://localhost:${PORT}/mcp`);
console.log(`WARNING: No DNS rebinding protection enabled!`);
});