|
| 1 | +/* ---- SSRF guard: never POST to internal/loopback/link-local addresses ---- */ |
| 2 | +export function isInternalUrl(raw: string): boolean { |
| 3 | + let u: URL; |
| 4 | + try { u = new URL(raw); } catch { return true; } |
| 5 | + if (u.protocol !== "https:" && u.protocol !== "http:") return true; |
| 6 | + if (process.env.NODE_ENV === "production" && u.protocol !== "https:") return true; |
| 7 | + const h = u.hostname.toLowerCase(); |
| 8 | + if (h === "localhost" || h.endsWith(".localhost") || h === "metadata.google.internal") return true; |
| 9 | + if (h === "0.0.0.0" || h === "::1" || h === "[::1]") return true; |
| 10 | + const ipv6 = h.replace(/^\[|\]$/g, ""); |
| 11 | + if (ipv6.includes(":")) { |
| 12 | + const mapped = ipv6.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/); |
| 13 | + if (mapped) { |
| 14 | + const high = Number.parseInt(mapped[1]!, 16); |
| 15 | + const low = Number.parseInt(mapped[2]!, 16); |
| 16 | + const a = high >> 8; |
| 17 | + const b = high & 255; |
| 18 | + const c = low >> 8; |
| 19 | + if ( |
| 20 | + a === 127 || |
| 21 | + a === 10 || |
| 22 | + a === 0 || |
| 23 | + (a === 172 && b >= 16 && b <= 31) || |
| 24 | + (a === 192 && b === 168) || |
| 25 | + (a === 169 && b === 254) |
| 26 | + ) return true; |
| 27 | + if (a || b || c || low) return false; |
| 28 | + } |
| 29 | + if ( |
| 30 | + ipv6 === "::" || |
| 31 | + ipv6 === "::1" || |
| 32 | + ipv6.startsWith("fe80:") || |
| 33 | + ipv6.startsWith("fc") || |
| 34 | + ipv6.startsWith("fd") || |
| 35 | + ipv6.startsWith("::ffff:10.") || |
| 36 | + ipv6.startsWith("::ffff:127.") || |
| 37 | + ipv6.startsWith("::ffff:192.168.") || |
| 38 | + /^::ffff:172\.(1[6-9]|2\d|3[01])\./.test(ipv6) || |
| 39 | + ipv6.startsWith("::ffff:169.254.") |
| 40 | + ) return true; |
| 41 | + } |
| 42 | + const m = h.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/); |
| 43 | + if (m) { |
| 44 | + const [a, b] = [Number(m[1]), Number(m[2])]; |
| 45 | + if (a === 127 || a === 10 || a === 0) return true; |
| 46 | + if (a === 172 && b >= 16 && b <= 31) return true; |
| 47 | + if (a === 192 && b === 168) return true; |
| 48 | + if (a === 169 && b === 254) return true; |
| 49 | + } |
| 50 | + return false; |
| 51 | +} |
0 commit comments