Security Reference
MCP server DNS rebinding: localhost attack and host header validation defense
DNS rebinding lets an attacker control a domain whose IP address resolves first to an attacker server and then to 127.0.0.1. The browser's same-origin policy then treats the attacker's page as same-origin with your localhost MCP server — allowing arbitrary cross-origin requests to your local process.
Why MCP servers on localhost are uniquely exposed
Claude Code and other MCP clients frequently run tool servers locally on 127.0.0.1, often on a fixed port like 3000 or 8080. These servers are not accessible to the public internet — but they are accessible from any browser tab running on the same machine. DNS rebinding bridges that gap.
The attack sequence:
- Attacker registers
rebind.evil.comwith a DNS TTL of 1 second - Victim visits
rebind.evil.com— DNS resolves to1.2.3.4(attacker server) - Attacker page loads JavaScript; 1 second later, DNS TTL expires
- Attacker's DNS server changes
rebind.evil.com→127.0.0.1 - JavaScript fetches
http://rebind.evil.com:3000/mcp/tools— browser resolves to localhost - Same-origin policy allows the fetch — origin is still
rebind.evil.com:3000 - Attacker now has full read/write access to the victim's local MCP server
Real-world impact: An MCP server with file-system access (read, write, execute tools) grants an attacker performing DNS rebinding the ability to read arbitrary files, execute shell commands, or exfiltrate credentials from the victim's home directory — all from a browser tab the victim left open.
Defense 1: host header allowlist
The most reliable defense: check the Host header on every request and reject anything not on the allowlist. A DNS rebinding attack uses Host: rebind.evil.com — your allowlist will reject it.
const ALLOWED_HOSTS = new Set([
'localhost',
'127.0.0.1',
'[::1]',
// Add your production hostname here if running hosted
process.env.MCP_HOSTNAME || ''
].filter(Boolean));
function hostGuard(req, res, next) {
// Strip port number for comparison
const host = (req.headers.host || '').split(':')[0].toLowerCase();
if (!ALLOWED_HOSTS.has(host)) {
// Log the attempt — rebinding probes are worth monitoring
console.warn(`DNS rebinding probe? Rejected Host: ${req.headers.host}`);
return res.status(421).json({
error: 'misdirected_request',
message: 'Host header not in allowlist'
});
}
next();
}
// Apply before all MCP routes
app.use(hostGuard);
Defense 2: CSRF token for state-changing operations
Host header allowlists stop DNS rebinding at the HTTP level. But for MCP servers that expose a management UI or REST API alongside the JSON-RPC endpoint, CSRF protection provides a second layer that's independent of DNS:
import crypto from 'crypto';
// On server startup, generate a token and bind it to this process instance
const CSRF_TOKEN = crypto.randomBytes(32).toString('hex');
// Expose it via a GET that requires same-origin (non-credentialed cross-origin can't read this)
app.get('/mcp/csrf-token', (req, res) => {
res.json({ token: CSRF_TOKEN });
});
// Require the token on all state-changing requests
function csrfGuard(req, res, next) {
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) return next();
const token = req.headers['x-csrf-token'] || req.body?._csrf;
if (token !== CSRF_TOKEN) {
return res.status(403).json({ error: 'invalid_csrf_token' });
}
next();
}
app.use(csrfGuard);
// MCP JSON-RPC endpoint — POST requires CSRF token
app.post('/mcp', csrfGuard, mcpHandler);
The MCP client (Claude Code) must fetch the CSRF token on startup and include it in subsequent requests. This is not a CSRF concern for stdio transport — only for HTTP/SSE transports.
Defense 3: bind to loopback only + document the port
An MCP server that binds to 0.0.0.0 is reachable from the local network, not just the machine. Bind to 127.0.0.1 explicitly:
// VULNERABLE: binds to all interfaces — reachable from LAN
app.listen(3000);
// SAFE: bind to loopback only
app.listen(3000, '127.0.0.1', () => {
console.log('MCP server listening on 127.0.0.1:3000');
});
Additionally: use a high, randomized port rather than a well-known port. An attacker performing DNS rebinding must guess or enumerate your port. Port 3000 is trivially guessable; port 47381 is not. Document your port choice in package.json:config.mcpPort so legitimate clients can discover it without hardcoding it.
Defense 4: Private Network Access (PNA) headers
Chrome's Private Network Access spec adds a CORS preflight for requests from public to private networks. If your MCP server implements the required response headers, Chrome will block DNS rebinding attacks at the browser level — even before your host guard runs:
// Respond to PNA preflight
app.options('*', (req, res) => {
if (req.headers['access-control-request-private-network']) {
// Only allow preflights from the exact origin you control
// Do NOT set this to '*' — that defeats the purpose
const allowedOrigins = ['http://localhost:3000', 'http://127.0.0.1:3000'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.set({
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Private-Network': 'true',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, X-CSRF-Token'
});
return res.sendStatus(204);
}
}
res.sendStatus(403);
});
Browser support: PNA headers are enforced in Chrome 104+ and Edge 104+. Firefox and Safari do not yet enforce PNA. Host header allowlisting remains the cross-browser reliable defense.
SkillAudit grading criteria
| Finding | Severity | Score impact |
|---|---|---|
| HTTP transport, no host header validation | HIGH | −20 |
| Server binds to 0.0.0.0 (all interfaces) | HIGH | −12 |
| State-changing HTTP endpoint with no CSRF protection | MEDIUM | −10 |
| Well-known port (3000, 8080, etc.) increases guessability | MEDIUM | −5 |
| Host header allowlist implemented | PASS | +8 |
| CSRF token on state-changing routes | PASS | +5 |
| PNA headers implemented | PASS | +5 |
Related SkillAudit checks
- SSRF security — DNS rebinding can be combined with SSRF to pivot from the victim's machine to internal services
- HTTP header injection — header injection can forge the Host header to bypass allowlist checks
- Permissions checklist — file-system and shell-exec permissions amplify the impact of a successful DNS rebinding attack
- Input validation patterns — URL validation must block
127.0.0.1references that could be used post-rebind