| 3 | * Prevents SSRF attacks by blocking local/private IP addresses and internal domains |
| 4 | */ |
| 5 | export function checkSafeUrl(url: string): boolean { |
| 6 | try { |
| 7 | const urlObj = new URL(url); |
| 8 | const hostname = urlObj.hostname.toLowerCase(); |
| 9 | |
| 10 | // Reject IPv6 addresses (IPv6 addresses are wrapped in brackets by URL object) |
| 11 | if (hostname.startsWith("[") && hostname.endsWith("]")) { |
| 12 | return false; |
| 13 | } |
| 14 | |
| 15 | // Reject IPv4 address format |
| 16 | const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/; |
| 17 | if (ipv4Regex.test(hostname)) { |
| 18 | return false; |
| 19 | } |
| 20 | |
| 21 | // Reject local domains and loopback addresses |
| 22 | const localDomains = ["localhost", "127.0.0.1", "0.0.0.0", "::1"]; |
| 23 | |
| 24 | if (localDomains.includes(hostname)) { |
| 25 | return false; |
| 26 | } |
| 27 | |
| 28 | // Reject .local domains |
| 29 | if (hostname.endsWith(".local")) { |
| 30 | return false; |
| 31 | } |
| 32 | |
| 33 | // Reject private IP address ranges (additional check in case IP format bypasses above) |
| 34 | if (ipv4Regex.test(hostname)) { |
| 35 | const parts = hostname.split(".").map(Number); |
| 36 | // 10.0.0.0/8 |
| 37 | if (parts[0] === 10) return false; |
| 38 | // 172.16.0.0/12 |
| 39 | if (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) return false; |
| 40 | // 192.168.0.0/16 |
| 41 | if (parts[0] === 192 && parts[1] === 168) return false; |
| 42 | // 127.0.0.0/8 (loopback) |
| 43 | if (parts[0] === 127) return false; |
| 44 | // 169.254.0.0/16 (link-local) |
| 45 | if (parts[0] === 169 && parts[1] === 254) return false; |
| 46 | } |
| 47 | |
| 48 | // Must contain at least one dot (ensure it's a valid domain, not a single word) |
| 49 | if (!hostname.includes(".")) { |
| 50 | return false; |
| 51 | } |
| 52 | |
| 53 | // Domain must have at least a top-level domain (exclude single dot cases) |
| 54 | const parts = hostname.split("."); |
| 55 | if (parts.length < 2 || parts.some((part) => part.length === 0)) { |
| 56 | return false; |
| 57 | } |
| 58 | |
| 59 | // Only allow http and https protocols |
| 60 | const allowedProtocols = ["http:", "https:"]; |
| 61 | if (!allowedProtocols.includes(urlObj.protocol)) { |
| 62 | return false; |