Topic: mcp server dns rebinding

MCP server DNS rebinding — attacking localhost-bound MCP servers

MCP servers that expose an HTTP interface on localhost are vulnerable to DNS rebinding attacks: a technique that lets an attacker-controlled web page make browser requests to your localhost server, bypassing the same-origin policy. The attack is particularly relevant for MCP servers running on developer machines, since those servers typically have access to credentials, local files, and internal network resources that the browser's same-origin policy is supposed to protect. The defenses are: Host header validation, strict CORS policy, Origin header allowlisting, and binding to 127.0.0.1 rather than 0.0.0.0.

How DNS rebinding works against MCP servers

The DNS rebinding attack has four steps:

  1. Victim visits attacker's page. The victim's browser loads a page from attacker.example.com, which resolves to the attacker's real server IP.
  2. Attacker changes DNS. The attacker's DNS server changes the A record for attacker.example.com to point to 127.0.0.1, with a very short TTL (often 0). After the original TTL expires, the browser re-resolves the domain.
  3. Browser makes request to localhost. The attacker's JavaScript code makes a fetch() request to http://attacker.example.com:PORT/tool. The browser resolves the domain again — now gets 127.0.0.1. Since the request is to the same "origin" (attacker.example.com), no CORS preflight is triggered (for simple requests). The request goes to your MCP server's port.
  4. MCP server responds. Your localhost MCP server receives the request from the browser with Host: attacker.example.com. If it doesn't validate the Host header, it processes the request and returns the tool result to the attacker's JavaScript.

Attack surface: why MCP servers are particularly exposed

The DNS rebinding attack targets any HTTP service listening on localhost. MCP servers on developer machines are an especially valuable target because they typically have access to: SSH keys and SSH agent forwarding, GitHub/GitLab personal access tokens in environment variables, AWS/GCP/Azure credentials in credential files, local file system with arbitrary read/write paths, database connections to local development databases, and credentials to internal services behind corporate VPNs.

A successful DNS rebinding attack against an MCP server is equivalent to a successful SSRF attack — the attacker can invoke any tool the server exposes, with the server's full credential context.

Defense 1: Host header validation

import http from 'http'

const ALLOWED_HOSTS = new Set([
  'localhost',
  '127.0.0.1',
  `localhost:${PORT}`,
  `127.0.0.1:${PORT}`,
])

function hostValidationMiddleware(
  req: http.IncomingMessage,
  res: http.ServerResponse,
  next: () => void
) {
  const host = req.headers.host ?? ''

  // Strip port for comparison if needed
  const hostWithoutPort = host.replace(/:\d+$/, '')

  if (!ALLOWED_HOSTS.has(host) && !ALLOWED_HOSTS.has(hostWithoutPort)) {
    // DNS rebinding: Host header contains attacker's domain, not localhost
    res.writeHead(400, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'Invalid Host header' }))
    return
  }

  next()
}

// Attach to every route — including the /mcp endpoint
server.use(hostValidationMiddleware)

Defense 2: Strict CORS policy with Origin allowlist

// WRONG: wildcard CORS — any origin can read your MCP server responses
app.use((req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*')
  next()
})

// CORRECT: explicit origin allowlist
const ALLOWED_ORIGINS = new Set([
  'vscode-webview://claude-code',  // VS Code extension WebView
  'null',                           // Native app / Electron (no-origin requests)
  // Add other known MCP client origins here
])

function corsMiddleware(
  req: http.IncomingMessage,
  res: http.ServerResponse,
  next: () => void
) {
  const origin = req.headers.origin ?? 'null'

  if (ALLOWED_ORIGINS.has(origin)) {
    res.setHeader('Access-Control-Allow-Origin', origin)
    res.setHeader('Vary', 'Origin')
  }
  // If origin is not in allowlist: do NOT set Access-Control-Allow-Origin
  // Browser blocks the response — attacker's JavaScript cannot read the result

  if (req.method === 'OPTIONS') {
    // Preflight: return 204 with CORS headers (only if origin is allowed)
    if (ALLOWED_ORIGINS.has(origin)) {
      res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS')
      res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization')
      res.setHeader('Access-Control-Max-Age', '86400')
    }
    res.writeHead(204)
    res.end()
    return
  }

  next()
}

Defense 3: Bind to 127.0.0.1, not 0.0.0.0

// WRONG: binds to 0.0.0.0 — reachable from any interface, including network interfaces
// (exposes the MCP server to other machines on the same LAN/VPN)
server.listen(PORT)
server.listen(PORT, '0.0.0.0')

// CORRECT: bind explicitly to loopback interface only
server.listen(PORT, '127.0.0.1', () => {
  console.log(`MCP server listening on 127.0.0.1:${PORT}`)
  // Not reachable from other machines — only from the local process and browser
})

// For IPv6 environments, also bind to ::1 if needed:
// server.listen(PORT, '::1')

Defense 4: Require a shared secret for browser-originated requests

// For MCP servers that accept requests from a web UI or browser extension,
// require a pre-shared token that the attacker's JavaScript cannot know.
// Browser's CORS policy prevents the attacker from reading the token
// from a legitimate client session, but if CORS is misconfigured this is
// an additional layer.

import crypto from 'crypto'

// Generated at server start; the MCP client receives this during connection setup
const SESSION_TOKEN = crypto.randomBytes(32).toString('hex')

function tokenMiddleware(
  req: http.IncomingMessage,
  res: http.ServerResponse,
  next: () => void
) {
  // DNS rebinding attacks arrive without this token
  // (the attacker's JavaScript cannot extract it from the legitimate client)
  const providedToken = req.headers['x-mcp-session-token']

  if (providedToken !== SESSION_TOKEN) {
    res.writeHead(401, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ error: 'Invalid or missing session token' }))
    return
  }

  next()
}

// Note: custom request headers trigger CORS preflight for cross-origin requests.
// The preflight will be rejected by your CORS policy (Defense 2) — so this is
// belt-and-suspenders, not the primary defense.

What SkillAudit checks

See also

Check your localhost MCP server for DNS rebinding exposure and CORS misconfigurations.

Run a free audit → How grading works →