DNS rebinding in local MCP servers: how to check and fix it
A local MCP server that speaks HTTP (streamable HTTP or SSE) and does not check the Host header can be reached by any web page you open. The page does not need to break out of the browser: it makes the browser treat your server as part of the page's own site. This page shows how that works, a one-line check and the fixes. Last checked: 12 October 2026, against the MCP TypeScript SDK 1.26.0.
How the attack works
- You run an MCP server on your machine, for example on
http://127.0.0.1:3000/mcp. - You open a web page on a domain the attacker controls, say
rebind.example. Its DNS record has a very short lifetime. - After the page loads, the attacker changes the record so
rebind.examplepoints at127.0.0.1. - Script on the page now calls
http://rebind.example:3000/mcp. The browser sees the same origin as the page, so the same-origin policy does not stop it and no CORS check applies. The request reaches your server. - The one thing that gives it away is the Host header: it says
rebind.example:3000, not127.0.0.1:3000orlocalhost:3000.
If the server ignores Host, the page can start a session, list the tools and call them, with whatever access those tools have: files, shell commands, tokens in the server's environment. Stdio servers are not exposed this way, since they have no port. Recent Chrome versions ask before a public site reaches a local address, but do not count on every browser, or every user, saying no.
How to check your server
Send a request with a Host header your server should never accept:
curl -i -H 'Host: rebind.example' http://127.0.0.1:3000/mcp
Use your own port and path. A server that checks Host refuses before it reads the request: the TypeScript SDK answers 403 with Invalid Host header, and the Python SDK's transport security answers 421. Any other status (200, or a 400, 405 or 406 complaining about the request) means the Host header was not checked.
Check the Origin header too. A browser sends Origin on these requests, so a server that checks it can refuse a page it does not know:
curl -i -H 'Origin: http://rebind.example' http://127.0.0.1:3000/mcp
With the protection on and an Origin allow-list set, the TypeScript SDK answers 403 with Invalid Origin header. Origin is a second line, not a replacement: requests that do not come from a browser can leave it out.
Also check where the server listens. lsof -iTCP -sTCP:LISTEN -n -P (macOS, Linux) shows each port and its address; 0.0.0.0 or * means every network interface, not just this machine.
How to fix it
- Allow only the Hosts you expect. Accept
127.0.0.1:<port>andlocalhost:<port>(and[::1]:<port>if you listen on IPv6), and refuse everything else with 403 or 421 before the request reaches any MCP code. - Turn on the SDK's protection, with its list. In the TypeScript SDK's streamable HTTP transport,
allowedHostsonly takes effect withenableDnsRebindingProtection: true; with the flag off, the list is ignored. The values are matched exactly, port included:
new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
enableDnsRebindingProtection: true,
allowedHosts: ['127.0.0.1:3000', 'localhost:3000'],
allowedOrigins: ['http://localhost:3000'],
});
- Or use the SDK's Express helper.
createMcpExpressApp()adds Host checking for localhost names (itshostoption defaults to127.0.0.1). It does not open the port itself, so listen on the same address:app.listen(3000, '127.0.0.1'); a bareapp.listen(3000)listens on every interface. Withhost: '0.0.0.0'it adds no Host check unless you passallowedHosts, which here are names without the port. - In Python, set the transport security settings:
enable_dns_rebinding_protection,allowed_hostsandallowed_origins. - Bind to 127.0.0.1, not 0.0.0.0. Listening on every interface lets any machine on the same network call the tools directly, with no rebinding needed. A Host allow-list does not help there, since that caller can send any Host it likes.
- Re-run the check after every SDK upgrade or config change. Protection that depends on a flag is easy to lose in a refactor.
A token or session secret on the server helps as well, but it is not a substitute: if the page can read a response that contains it, the secret is gone.
Lint your configs for it
The free MCP Safety Lint 1.0.1 (SHA-256) has a lint_config tool whose dns-rebinding rule flags an HTTP or SSE server config with no Host allow-list or with the protection turned off, and whose all-interfaces rule flags a server listening on 0.0.0.0; it runs offline, and the tool-poisoning guide shows how to add it.
Related: Hostile clients and L402 MCP gates, MCP conformance vs safety and testing MCP clients against tool poisoning.