/** * URL and IP validation for PDS ingestion. * Prevents SSRF by enforcing https: and rejecting private/reserved IPs. */ /** IPv4 ranges that must never be contacted (SSRF protection) */ const PRIVATE_IP_RANGES: Array<{ prefix: string; mask: number }> = [ // Loopback { prefix: "127.", mask: 0 }, // 10.0.0.0/8 { prefix: "10.", mask: 0 }, // 172.16.0.0/12 { prefix: "172.", mask: 16 }, // 192.168.0.0/16 { prefix: "192.168.", mask: 0 }, // Link-local { prefix: "169.254.", mask: 0 }, // Current network { prefix: "0.", mask: 0 }, ]; /** Cloud metadata IPs to block explicitly */ const BLOCKED_IPS = new Set([ "169.254.169.254", // AWS/GCP/Azure metadata "100.100.100.200", // Alibaba Cloud metadata "fd00:ec2::254", // AWS IMDSv2 IPv6 ]); /** Check if an IP address is in a private/reserved range */ export function isPrivateIp(ip: string): boolean { if (BLOCKED_IPS.has(ip)) return true; // IPv6 private ranges if ( ip.startsWith("::1") || ip.startsWith("fc") || ip.startsWith("fd") || ip.startsWith("fe80") ) { return true; } // IPv4 checks for (const range of PRIVATE_IP_RANGES) { if (!ip.startsWith(range.prefix)) continue; // 172.16.0.0/12: second octet must be 16-31 if (range.prefix === "172." && range.mask === 16) { const secondOctet = parseInt(ip.split(".")[1], 10); if (secondOctet >= 16 && secondOctet <= 31) return true; continue; } return true; } return false; } /** Validate a PDS URL is safe to store and fetch */ export function isValidPdsUrl(url: string): boolean { let parsed: URL; try { parsed = new URL(url); } catch { return false; } // Must be https if (parsed.protocol !== "https:") return false; // No userinfo (user:pass@host) if (parsed.username || parsed.password) return false; // Hostname must not be an IP literal pointing to private range if (isPrivateIp(parsed.hostname)) return false; // Reject bare IPs — PDS servers should have real hostnames if (/^\d{1,3}(\.\d{1,3}){3}$/.test(parsed.hostname)) return false; // Must have a valid hostname with at least one dot (no localhost etc.) if (!parsed.hostname.includes(".")) return false; return true; } /** Strip trailing slashes from a PDS URL to prevent double-slash in paths */ export function normalizePdsUrl(url: string): string { return url.replace(/\/+$/, ""); } /** Validate a URL is safe to render in an href attribute */ export function isSafeHref(url: string): boolean { try { const parsed = new URL(url); return parsed.protocol === "https:" || parsed.protocol === "http:"; } catch { return false; } }