DNS Rebinding Attacks: How a Website Reaches Your localhost
DNS rebinding lets a malicious web page send requests to services on your machine or private network — dev servers, admin panels, local MCP servers, routers — despite the same-origin policy. How the attack works step by step, why SSRF filters fall to it too, and the defences: Host header checks, authentication, binding, and resolve-then-pin.
You're running a dev server on localhost:3000, or a local AI tool on 127.0.0.1:8080, or an MCP server that can run shell commands. It isn't exposed to the internet, so it doesn't need authentication. Right?
DNS rebinding says otherwise. It lets a web page you visit — any page, in an ad, in a background tab — send requests to services on your machine and your private network, and read the responses. (What is localhost)
Why the same-origin policy doesn't stop it
Browsers isolate sites by origin: scheme + host + port. JavaScript on https://evil.example can't read responses from http://localhost:3000, because those are different origins. (CORS errors explained)
The trick: the origin is based on the hostname, not the IP address. If evil.example first resolves to the attacker's server and later resolves to 127.0.0.1, the browser still considers it the same origin — and lets the page read the responses.
The attack, step by step
- You visit
http://rebind.evil.example:3000/. The attacker's DNS server answers with their own IP and a very short TTL (say, 1 second). - The page loads attacker JavaScript.
- The script waits for the DNS record to expire, then makes a request:
fetch('http://rebind.evil.example:3000/api/secrets'). - The browser resolves the name again. This time the attacker's DNS server answers
127.0.0.1. - The request goes to your localhost:3000. Same origin as far as the browser is concerned, so the script can read the response and send it home.
The local service sees an ordinary HTTP request. The only clue is the header Host: rebind.evil.example:3000.
Browsers cache DNS and try to resist this, so real attack tools (like NCC Group's Singularity) use tricks — multiple A records, cache flooding, repeated attempts — to make it reliable. It typically takes seconds to a minute. Port is chosen by the attacker, so any local port is reachable.
What's at risk
- Development servers with debug endpoints, unauthenticated admin routes, or source maps and environment info.
- Local AI tooling: model servers, agent dashboards, and especially local MCP servers using HTTP transports. A rebinding page that can call a tool that runs shell commands or reads files has code execution on your machine. The MCP specification explicitly tells servers to validate the
Originheader and bind to localhost for this reason. (Secure MCP servers) - Databases and caches with HTTP interfaces and no auth (Elasticsearch, some admin UIs).
- Routers, NAS devices, printers, IoT on the home or office network — many have had rebinding-exploitable admin panels.
- Cloud metadata services when the browser runs on a cloud VM (remote desktops, browser-based IDEs).
- Kubernetes dashboards, Docker APIs exposed on internal addresses. (Docker permission denied on the socket)
Notable real-world cases include local daemons for desktop apps, crypto wallets, game clients, and developer tools that assumed "localhost-only" meant "trusted".
Rebinding and SSRF
DNS rebinding also defeats naive SSRF defences on the server side. A typical filter:
// ❌ check-then-use
const { address } = await dns.lookup(host)
if (isPrivate(address)) throw new Error('blocked')
await fetch(`http://${host}/...`) // resolves AGAIN — may get 169.254.169.254
The validation and the actual request do separate DNS lookups. An attacker's DNS server returns a public IP to the first and an internal one to the second — a time-of-check/time-of-use bug. (SSRF explained)
The fix is to resolve once, validate, and connect to that exact IP (passing the original hostname for TLS SNI and the Host header), or to route all outbound requests through an egress proxy that does the check at connection time. (Agent egress control proxy)
Defences for local services
1. Validate the Host header
The most effective single defence. A rebinding request carries the attacker's hostname in Host. If your service only accepts localhost, 127.0.0.1 and its real hostnames, the attack fails:
const ALLOWED = new Set(['localhost:3000', '127.0.0.1:3000', '[::1]:3000'])
app.use((req, res, next) => {
if (!ALLOWED.has(req.headers.host)) return res.status(421).send('Misdirected')
next()
})
Many dev servers do this by default now. Vite answers only localhost, *.localhost and IP addresses unless you add hosts to server.allowedHosts — and its docs warn that setting it to true lets any website read your source code through DNS rebinding. webpack-dev-server has the same allowedHosts option. Next.js blocks cross-origin requests to its dev-only endpoints unless the requesting origin is in allowedDevOrigins. Don't open any of these up to "all" to make a tunnel work — add the specific tunnel hostname instead. (Cloudflare Tunnel)
2. Check Origin on state-changing requests
Requests made by fetch from a page include an Origin header. Reject requests whose Origin isn't one you serve. This also blocks plain cross-site requests from attacker pages (CSRF-style attacks on localhost). (CSRF explained)
3. Require authentication — even on localhost
A random token generated at startup and required on every request (Jupyter's approach) defeats rebinding, because the attacker's page doesn't know it. Cookies don't help here — the rebound origin is the attacker's hostname, so your service's cookies aren't sent.
4. Bind narrowly
Bind to 127.0.0.1, not 0.0.0.0, unless you need LAN access. This doesn't stop rebinding (which targets 127.0.0.1), but it stops other machines on your network from reaching the service directly. (ERR_CONNECTION_REFUSED)
5. Use HTTPS or Unix sockets where possible
A rebinding attack against an HTTPS service fails certificate validation, because the attacker's hostname doesn't match your local certificate. Unix domain sockets aren't reachable from browsers at all.
Browser and network defences
- Local Network Access — since Chrome 142 (October 2025), Chrome and other Chromium browsers ask the user for permission before a public website can send requests to local-network or loopback addresses. It replaced the earlier Private Network Access preflight effort, which was put on hold. Because the target's address space is decided from the IP it resolves to at request time, a rebound request to
127.0.0.1should hit that prompt. It's a strong extra layer, but not one to rely on: users click "Allow", other browsers behave differently, and coverage of WebSockets and WebRTC is still being added. - DNS rebinding protection in resolvers — dnsmasq (
--stop-dns-rebind), Unbound (private-address), Pi-hole and many routers can refuse public DNS answers that point to private IP ranges. Good for a home or office network; doesn't coverlocalhostin every configuration. - Browser DNS pinning varies and has never been a complete defence.
A checklist for anything listening on localhost
- Does it validate
Hostagainst an allow-list? - Does it check
Originon requests that change state or return secrets? - Does it require a token, even locally?
- Is it bound to
127.0.0.1rather than all interfaces? - Does any SSRF filter resolve once and connect to the validated IP?
If your local AI agent tooling fails these, a web page can drive it. (Prompt injection in coding agents)
On EasySpawn, Claude Code and your dev servers run on a remote VM rather than on your laptop, so a malicious page in your browser can't rebind its way into them through localhost. See how it works or join the waitlist.
Related: SSRF Explained · Secure MCP Servers · CORS Errors Explained · What Is localhost?
Keep reading
WebAssembly as a Sandbox: Running Untrusted Code With Wasmtime and WASI
WebAssembly's design makes it a strong in-process sandbox: linear memory, no ambient authority, and capability-based access through WASI. How the isolation works, limiting CPU with fuel and epochs, memory limits, the component model, real uses for plugins and user code, and where it falls short.
Mutual TLS (mTLS) Explained: When the Server Checks You Too
In normal TLS only the server proves its identity. With mutual TLS the client presents a certificate too. How the mTLS handshake works, running a private CA, issuing and rotating short-lived client certificates, configuring Nginx and Node, mTLS in service meshes and webhooks, and the operational traps.