@@ -51,6 +52,55 @@ const PRIVATE_IP_RANGES = [ ]; export class SSRFProtection {
Show 12 hidden lines
+ /**+ * IPv6 addresses that must be blocked: loopback, unspecified, link-local,+ * unique-local, site-local (deprecated), IPv4-mapped, IPv4-compatible,+ * 6to4, and NAT64-mapped addresses. All of these either represent private+ * networks or embed an arbitrary IPv4 address that would bypass IPv4-only+ * SSRF checks.+ *+ * Hostname must be lowercased and bracket-stripped. WHATWG URL parser+ * canonicalizes IPv6 literals (zero compression, dotted-quad → hex pairs),+ * so prefix matching works against the normalized form.+ *+ * @security See GHSA-56c3-vfp2-5qqj. The sync validator previously had no
+ * IPv6 gate, letting `::ffff:169.254.169.254`, `::169.254.169.254`,+ * `2002:a9fe:a9fe::`, and `64:ff9b::a9fe:a9fe` reach the HTTP client.+ */+ private static isPrivateOrMappedIpv6(hostname: string): boolean {+ // Gate on net.isIPv6 so domain names starting with hex-like labels+ // (e.g. "fcexample.com") are never misclassified as private IPv6.+ if (!isIPv6(hostname)) return false;
Show 22 hidden lines
++ // ::/96 reserved block: unspecified (`::`), loopback (`::1`), IPv4-mapped+ // (`::ffff:X`), and deprecated IPv4-compatible (`::X:Y` per RFC 4291) all+ // live here. Blocking the whole prefix avoids enumerating subforms.+ if (hostname.startsWith('::')) return true;++ // Defensive long-form IPv4-mapped — WHATWG URL normally compresses this,+ // but keep the check in case normalization ever changes.+ if (hostname.startsWith('0:0:0:0:0:ffff:')) return true;++ // Link-local fe80::/10+ if (hostname.startsWith('fe80:')) return true;++ // Site-local fec0::/10 (deprecated, RFC 3879) — still honored by some stacks.+ if (/^fe[c-f]/.test(hostname)) return true;++ // Unique local fc00::/7 (RFC 4193). Covers fc00-fdff in the first hextet.+ if (/^f[cd]/.test(hostname)) return true;++ // 6to4 2002::/16 (RFC 3056) — bits 16-47 embed an arbitrary IPv4 address,+ // so any 2002: address can tunnel to RFC1918 or metadata endpoints.+ if (hostname.startsWith('2002:')) return true;
++ // NAT64 64:ff9b::/96 (RFC 6052) and 64:ff9b:1::/48 (RFC 8215) — embedded+ // IPv4 in the low 32 bits, same tunneling concern as 6to4.+ if (hostname.startsWith('64:ff9b:')) return true;++ return false;+ }
Show 4 hidden lines
+ /** * Validate webhook URL for SSRF protection with configurable security modes *
The closing patch adds a dedicated classifier for mapped, compatible, local, 6to4, and NAT64 IPv6 representations that the earlier IPv4 expressions could not recognize.