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