EXERCISE 2 — URL validation defence against SSRF (blocklist + DNS rebinding) =========================================================================== DESIGNING URL VALIDATION DEFENCE: The goal: given a URL provided by an attacker, determine if it's safe for the server to fetch. STRATEGY: "block dangerous, allow safe" — explicitly block known-dangerous IP ranges and hostnames; only allow specific, whitelisted domains/IPs. --- 5 DANGEROUS IP RANGES / HOSTNAMES TO BLOCK (and why): 1. LOCALHOST / LOOPBACK (127.0.0.1, ::1, "localhost") Why dangerous: localhost resolves to the server's own machine. If blocked, attacker can't access services on the same server (databases, admin dashboards, local HTTP services on ports 8080, 9000, etc.). Attack if allowed: attacker requests http://localhost:27017 (MongoDB), reads DB. Mitigation: reject any URL that resolves to 127.0.0.0/8 or ::1. 2. PRIVATE IP RANGES (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) Why dangerous: these are the standard private networks used in corporate environments. If blocked, attacker can't access internal services on private IPs (internal APIs, databases, dashboards). Attack if allowed: attacker requests http://10.0.0.5:8080 (internal API), exfiltrates data. Mitigation: reject any URL that resolves to private IP ranges. 3. LINK-LOCAL / SELF-IDENTIFICATION (169.254.0.0/16, 0.0.0.0/8) Why dangerous: 169.254.x.x is the link-local range; 169.254.169.254 is the AWS metadata service (also used by Google Cloud, Azure — 169.254.169.254). 0.0.0.0 refers to "this machine." Attack if allowed: attacker requests http://169.254.169.254/latest/meta-data/, obtains AWS credentials. Mitigation: reject any URL that resolves to 169.254.0.0/16 or 0.0.0.0/8. 4. RESERVED / SPECIAL RANGES (224.0.0.0/4, 240.0.0.0/4, 127.0.0.1, ::1) Why dangerous: these are reserved for multicast (224–239.x.x) and experimental (240–255.x.x) use. Blocking prevents unknown weird scenarios. Attack if allowed: rare, but multicast/experimental ranges might have hidden services or be used in novel attacks. Mitigation: reject reserved and experimental ranges as a general rule. 5. HOSTNAME "localhost" + COMMON INTERNAL DOMAIN PATTERNS Why dangerous: even if DNS resolution to 127.0.0.1 is blocked, if the hostname itself is "localhost," it's a red flag. Same for patterns like "internal-api", "admin", "dashboard", "localhost.localdomain". Attack if allowed: attacker uses hostname "internal-api" which resolves to a private IP; if you only block IPs (not hostnames), this bypasses the check. Mitigation: also block known dangerous hostnames. --- WHY BLOCKING BY HOSTNAME ALONE IS INSUFFICIENT (DNS REBINDING ATTACK): SCENARIO: Your URL validation checks the HOSTNAME: "Is the hostname whitelisted?" Attacker provides: http://attacker-website.com/ Validation: "attacker-website.com? That's not in the whitelist. Let me check if it's dangerous..." -> Validation sees: resolves to attacker's IP (e.g., 203.0.113.42). That's not private, not localhost, so ALLOWED. BUT: attacker-website.com is actually a DOMAIN THEY CONTROL. They set the DNS record to point to THEIR IP (203.0.113.42) initially. ATTACK FLOW (DNS rebinding): 1. Your code checks the hostname: attacker-website.com -> resolves to 203.0.113.42 (attacker's server). Validation says: "OK, that's external, not dangerous." 2. Your code makes the HTTP request to http://attacker-website.com/. 3. The attacker's server receives the request and sends back a redirect (HTTP 302): Location: http://localhost:27017 (or http://169.254.169.254/latest/meta-data/) 4. Your HTTP client follows the redirect (if configured to do so). 5. Your code is now requesting http://localhost:27017 — SSRF! WHY THIS WORKS: - You validated the HOSTNAME (attacker-website.com), not the RESOLVED IP. - The attacker controlled the redirect, so they changed the target after validation. - Your HTTP client followed the redirect without re-validating the new URL. --- FIX: VALIDATE AFTER DNS RESOLUTION: CORRECT APPROACH: 1. User/attacker provides: URL = http://attacker-website.com/ 2. Validate the hostname: whitelisted? Not in blacklist? -> OK 3. RESOLVE the hostname to an IP address. 4. VALIDATE THE RESOLVED IP: is it private, localhost, metadata service? -> BLOCK 5. Make the HTTP request to the resolved IP (not the hostname) to prevent re-rebinding. 6. If the server sends a redirect, VALIDATE THE NEW URL before following. CODE LOGIC (pseudocode): ``` url = "http://attacker-website.com/" parsed_url = parse(url) hostname = parsed_url.hostname // "attacker-website.com" // Step 1: Validate hostname against whitelist/blacklist if hostname in BLACKLIST or hostname == "localhost": REJECT // Step 2: Resolve hostname to IP ip = resolve_dns(hostname) // "203.0.113.42" // Step 3: Validate the resolved IP if ip in PRIVATE_RANGES or ip == "127.0.0.1" or ip in METADATA_RANGES: REJECT // Step 4: Make request response = http_fetch(url) // Step 5: If redirect, validate the new URL if response.status_code in [301, 302, 303, 307, 308]: redirect_url = response.headers['Location'] // Recursively validate the redirect URL (back to step 1) VALIDATE(redirect_url) ``` --- ADDITIONAL DEFENCES (beyond hostname validation): - DISABLE REDIRECTS: don't follow HTTP redirects; fail if a redirect is received. (This kills the DNS-rebinding attack.) Or: validate redirects (don't allow redirects to private IPs). - DISABLE DANGEROUS SCHEMES: block file://, gopher://, dict://, etc. Only allow http:// and https://. - CHECK RESOLVED IP BEFORE AND AFTER: if DNS changes between validation and request (unlikely in local resolution, but possible with TTL-0 records), you're protected. - USE A RESOLVER YOU TRUST: use a local recursive resolver (bind, unbound) on the same machine, not a public resolver, to prevent attacker DNS spoofing. --- ONE-LINE TAKEAWAY: Block private IPs (10.x, 172.16–31.x, 192.168.x) · localhost · metadata (169.254.x.x) · validate the RESOLVED IP (not just the hostname) to prevent DNS rebinding; disable redirects or validate redirects.