import { createHash } from "crypto"; import dns from "dns/promises"; const BLOCKED_TLDS = [".local", ".internal", ".corp", ".lan"]; const BLOCKED_HOSTNAMES = ["localhost", "localhost."]; /** * Checks whether an IP address is in a private/reserved range. */ function isPrivateIP(ip: string): boolean { // IPv4 if (ip === "0.0.0.0") return true; if (ip.startsWith("127.")) return true; // 127.0.0.0/8 if (ip.startsWith("10.")) return true; // 10.0.0.0/8 if (ip.startsWith("192.168.")) return true; // 192.168.0.0/16 if (ip.startsWith("169.254.")) return true; // 169.254.0.0/16 (link-local + cloud metadata) // 172.16.0.0/12: 172.16.x.x – 172.31.x.x if (ip.startsWith("172.")) { const second = parseInt(ip.split(".")[1] ?? "", 10); if (second >= 16 && second <= 31) return true; } // IPv6 — normalize: strip zone ID (%eth0) and lowercase const ip6 = ip.replace(/%.*$/, "").toLowerCase(); if (ip6 === "::1" || ip6 === "::") return true; if (ip6.startsWith("fe80")) return true; // fe80::/10 link-local if (ip6.startsWith("fc") || ip6.startsWith("fd")) return true; // fc00::/7 unique-local (ULA) if (ip6.startsWith("fd00:ec2::")) return true; // AWS EC2 metadata IPv6 if (ip6 === "::ffff:127.0.0.1") return true; if (ip6.startsWith("::ffff:")) { // IPv4-mapped IPv6 — extract the IPv4 part and re-check const v4 = ip6.slice(7); return isPrivateIP(v4); } // IPv4-compatible IPv6 (deprecated but still reachable) if (ip6.startsWith("::") && ip6.includes(".")) { const v4 = ip6.slice(2); return isPrivateIP(v4); } return false; } /** * Validates a monitor URL is safe to fetch (not targeting internal resources). * Returns null if safe, or an error string if blocked. */ export async function validateMonitorUrl(url: string): Promise { let parsed: URL; try { parsed = new URL(url); } catch { return "Invalid URL"; } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { return `Blocked scheme: ${parsed.protocol} — only http: and https: are allowed`; } const hostname = parsed.hostname.toLowerCase(); if (BLOCKED_HOSTNAMES.includes(hostname)) { return "Blocked hostname: localhost is not allowed"; } for (const tld of BLOCKED_TLDS) { if (hostname.endsWith(tld)) { return `Blocked TLD: ${tld} is not allowed`; } } try { const ips: string[] = []; try { const v4 = await dns.resolve4(hostname); ips.push(...v4); } catch {} try { const v6 = await dns.resolve6(hostname); ips.push(...v6); } catch {} if (ips.length === 0) { return "Could not resolve hostname"; } for (const ip of ips) { if (isPrivateIP(ip)) { return `Blocked: ${hostname} resolves to private/reserved IP ${ip}`; } } } catch { return "DNS resolution failed"; } return null; }