Security reference · DNS · SSRF
MCP server DNS caching security
DNS rebinding is a class of SSRF attack that exploits the gap between DNS resolution and TCP connection. An attacker registers a domain that initially resolves to an external IP address, passes it through an MCP tool's URL allowlist check, then changes the DNS record to an internal IP — 127.0.0.1, 169.254.169.254 (AWS metadata), or a private range. When the MCP server connects, it reaches the internal target even though the allowlist check approved the external address. This reference covers DNS rebinding variants, TTL manipulation mechanics, and the IP-level mitigations that close the attack window.
How DNS rebinding bypasses SSRF allowlists
Standard SSRF protection resolves the URL's hostname and checks the resulting IP against a blocklist of private ranges before making the request. DNS rebinding defeats this in two steps:
- Phase 1 (allowlist pass): The attacker controls
attacker.com, which resolves to an external IP like203.0.113.10with a very short TTL (0 or 1 second). The MCP server's SSRF check resolves the hostname, sees a public IP, and allows the request. - Phase 2 (rebind): Between the allowlist check and the actual fetch, the TTL expires. The attacker updates the DNS record to point to
127.0.0.1. The MCP server opens a new TCP connection toattacker.com— which now resolves to localhost.
The race window: Even with a 0-TTL DNS record, the rebind attack requires a race between the SSRF check resolution and the fetch resolution. In Node.js, dns.resolve() and the TCP connect in fetch() both call the OS resolver separately — there is no built-in TTL caching that would prevent re-resolution. The attack is reliably exploitable when there is any async work between the check and the fetch.
Localhost rebinding variant
The classic rebinding targets internal services on the MCP server's host or VPC. The most common target is:
127.0.0.1:8080— admin dashboard not exposed externally169.254.169.254— AWS/GCP instance metadata (IAM credentials, user data)10.0.0.0/8or192.168.0.0/16— other services in the VPC[::1]— IPv6 loopback, often missed by IPv4-only blocklists
The correct mitigation: resolve-then-connect with IP validation
The standard mitigation is to perform DNS resolution explicitly before the fetch, validate the resolved IP against a private-range blocklist, and then connect directly to the validated IP — bypassing the OS resolver on the actual connection. This ensures the IP checked is the IP connected to, with no rebind window between check and connect.
import dns from 'node:dns/promises';
import net from 'node:net';
import { IPCIDR } from 'ip-cidr'; // npm: ip-cidr
const BLOCKED_RANGES = [
'0.0.0.0/8',
'10.0.0.0/8',
'127.0.0.0/8',
'169.254.0.0/16', // link-local (AWS metadata endpoint)
'172.16.0.0/12',
'192.168.0.0/16',
'::1/128', // IPv6 loopback
'fc00::/7', // IPv6 private
'fe80::/10', // IPv6 link-local
];
function isPrivateIP(ip) {
for (const range of BLOCKED_RANGES) {
if (new IPCIDR(range).contains(ip)) return true;
}
return false;
}
async function safeFetch(rawUrl, options = {}) {
const parsed = new URL(rawUrl);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('FORBIDDEN_PROTOCOL: ' + parsed.protocol);
}
// Step 1: explicit DNS resolution
let addresses;
try {
addresses = await dns.resolve(parsed.hostname); // returns IPv4/IPv6 array
} catch {
throw new Error('DNS_RESOLUTION_FAILED');
}
if (!addresses.length) throw new Error('NO_DNS_RESULT');
// Step 2: check every resolved address against private IP ranges
for (const addr of addresses) {
if (isPrivateIP(addr)) {
throw new Error('SSRF_BLOCKED: ' + addr + ' is in a private IP range');
}
}
// Step 3: connect directly to the validated IP, not the hostname
// This prevents re-resolution at connect time
const validatedIP = addresses[0];
const connectUrl = rawUrl.replace(parsed.hostname, net.isIPv6(validatedIP) ? '[' + validatedIP + ']' : validatedIP);
return fetch(connectUrl, {
...options,
headers: {
...options.headers,
// Restore original Host header so virtual hosting works
'Host': parsed.host,
},
// Additional: set a short connect timeout
signal: AbortSignal.timeout(5000),
});
}
IPv6 bypass: If your SSRF blocklist only checks IPv4 addresses, an attacker can register a domain that resolves only to ::1 (IPv6 loopback). Always include IPv6 private ranges in your blocklist and check dns.resolve6() separately from dns.resolve4().
DNS TTL pinning
An alternative mitigation is to implement local DNS caching that respects only a minimum TTL — never honoring TTLs below 30 seconds. This extends the window in which the checked IP matches the connected IP, reducing but not eliminating the rebind risk. This is a defense-in-depth measure, not a standalone fix:
import dns from 'node:dns/promises';
// Simple TTL-aware DNS cache — ignores TTLs < 30 seconds
const dnsCache = new Map();
const MIN_CACHE_TTL_MS = 30_000;
async function cachedResolve(hostname) {
const cached = dnsCache.get(hostname);
if (cached && cached.expiresAt > Date.now()) {
return cached.addresses;
}
// dns.resolve with TTL option (Node 17+)
const records = await dns.resolve4(hostname, { ttl: true });
const addresses = records.map(r => r.address);
const minTTL = Math.min(...records.map(r => r.ttl));
const effectiveTTL = Math.max(minTTL * 1000, MIN_CACHE_TTL_MS);
dnsCache.set(hostname, { addresses, expiresAt: Date.now() + effectiveTTL });
return addresses;
}
DNS rebinding variation: browser-based rebinding via MCP web UI
If an MCP server has a web UI served on localhost, attackers can use browser-based DNS rebinding to reach it from a malicious page. The attacker's page at attacker.com makes a fetch to attacker.com:7777 (the same port as the MCP web UI). The browser enforces same-origin policy using the hostname, not the IP. After rebinding, attacker.com resolves to 127.0.0.1, and the browser's fetch succeeds because the hostname matches.
Mitigations specific to MCP web UIs:
- Set
Hostheader validation on local admin endpoints — reject requests where the Host header is notlocalhostor127.0.0.1 - Bind admin UI to
127.0.0.1only, not0.0.0.0 - Require a CSRF token on all state-changing admin API calls
SkillAudit findings: DNS rebinding
Run a SkillAudit scan on your MCP server to detect SSRF and DNS rebinding vulnerabilities in fetch tool handlers automatically.