Block private network ranges to avoid SSRF.
(url: string)
| 55 | |
| 56 | /** Block private network ranges to avoid SSRF. */ |
| 57 | function isPrivateOrLocal(url: string): boolean { |
| 58 | let parsed: URL; |
| 59 | try { |
| 60 | parsed = new URL(url); |
| 61 | } catch { |
| 62 | return false; |
| 63 | } |
| 64 | const host = parsed.hostname.toLowerCase(); |
| 65 | if (host === 'localhost' || host === '0.0.0.0') return true; |
| 66 | // IPv4 literal checks |
| 67 | const ipv4 = host.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); |
| 68 | if (ipv4) { |
| 69 | const [, a, b] = ipv4.map(s => parseInt(s, 10)); |
| 70 | if (a === 127) return true; // loopback |
| 71 | if (a === 10) return true; // RFC1918 |
| 72 | if (a === 192 && b === 168) return true; // RFC1918 |
| 73 | if (a === 172 && b! >= 16 && b! <= 31) return true; // RFC1918 |
| 74 | if (a === 169 && b === 254) return true; // link-local |
| 75 | } |
| 76 | // IPv6 loopback + ULA |
| 77 | if (host === '::1' || host.startsWith('[::1') || host.startsWith('[fc') || host.startsWith('[fd')) return true; |
| 78 | return false; |
| 79 | } |
| 80 | |
| 81 | /** Strip HTML tags into readable text. Lightweight; doesn't preserve structure. */ |
| 82 | function htmlToText(html: string): string { |