import { describe, expect, it } from "vitest"; import { assertUrlAllowed, isBlockedHostname, isBlockedIP, isBlockedIPv4, isBlockedIPv6, UrlPolicyError } from "./ssrf.js"; describe("blocked hostnames", () => { it("blocks loopback / metadata / internal suffixes / bare labels", () => { for (const h of ["localhost", "LOCALHOST.", "metadata.google.internal", "instance-data", "kubernetes.default.svc", "foo.local", "db.internal", "printer.home.arpa", "node.maclustr.io", "x.ts.net", "1.0.0.10.in-addr.arpa", "intranet"]) expect(isBlockedHostname(h), h).toBe(true); }); it("allows public hosts", () => { for (const h of ["www.equinix.com", "example.org", "a.b.c.d.e.io", "8.8.8.8"]) expect(isBlockedHostname(h), h).toBe(false); }); }); describe("blocked IPv4 ranges", () => { it("private, loopback, link-local, CGNAT, multicast, reserved, documentation", () => { for (const ip of ["10.0.0.1", "10.255.255.255", "172.16.0.1", "172.31.255.254", "192.168.1.1", "127.0.0.1", "127.255.0.1", "169.254.169.254", "100.64.0.1", "100.127.255.255", "0.0.0.0", "224.0.0.1", "239.255.255.255", "240.0.0.1", "255.255.255.255", "192.0.2.10", "198.51.100.5", "203.0.113.9", "198.18.0.1", "192.0.0.1"]) expect(isBlockedIPv4(ip), ip).toBe(true); }); it("public addresses pass, including neighbours of blocked ranges", () => { for (const ip of ["8.8.8.8", "1.1.1.1", "172.32.0.1", "172.15.255.255", "100.128.0.1", "11.0.0.1", "192.169.0.1", "223.255.255.255", "51.161.112.61"]) expect(isBlockedIPv4(ip), ip).toBe(false); }); }); describe("blocked IPv6", () => { it("loopback, unspecified, link-local, unique-local, multicast, documentation", () => { for (const ip of ["::1", "::", "fe80::1", "fe9f::1", "feb0::1", "fc00::1", "fd12:3456::1", "ff02::1", "2001:db8::1"]) expect(isBlockedIPv6(ip), ip).toBe(true); expect(isBlockedIPv6("2606:4700::6810:84e5")).toBe(false); expect(isBlockedIPv6("2001:4860:4860::8888")).toBe(false); }); it("IPv4-mapped addresses follow the IPv4 policy (dotted and hex forms)", () => { expect(isBlockedIPv6("::ffff:127.0.0.1")).toBe(true); expect(isBlockedIPv6("::ffff:10.1.2.3")).toBe(true); expect(isBlockedIPv6("::ffff:8.8.8.8")).toBe(false); expect(isBlockedIPv6("::ffff:7f00:1")).toBe(true); // 127.0.0.1 expect(isBlockedIPv6("::ffff:a9fe:a9fe")).toBe(true); // 169.254.169.254 expect(isBlockedIPv6("::ffff:808:808")).toBe(false); // 8.8.8.8 expect(isBlockedIPv6("0:0:0:0:0:ffff:c0a8:1")).toBe(true); // 192.168.0.1 }); it("NAT64 well-known and local-use prefixes embed IPv4 and follow the IPv4 policy", () => { expect(isBlockedIPv6("64:ff9b::808:808")).toBe(false); // 8.8.8.8 via NAT64 stays reachable expect(isBlockedIPv6("64:ff9b::a00:1")).toBe(true); // 10.0.0.1 expect(isBlockedIPv6("64:ff9b::7f00:1")).toBe(true); // 127.0.0.1 expect(isBlockedIPv6("64:ff9b:1:0:0:0:a9fe:a9fe")).toBe(true); // local-use prefix, 169.254.169.254 expect(isBlockedIPv6("64:ff9b:1:0:0:0:808:808")).toBe(false); expect(isBlockedIPv6("64:ff9b:dead::")).toBe(true); // malformed NAT64 form is blocked }); it("isBlockedIP dispatches by family and rejects non-IPs", () => { expect(isBlockedIP("10.0.0.1")).toBe(true); expect(isBlockedIP("::1")).toBe(true); expect(isBlockedIP("not-an-ip")).toBe(true); expect(isBlockedIP("9.9.9.9")).toBe(false); }); }); describe("assertUrlAllowed (no DNS)", () => { const opts = { resolve: false }; it("rejects bad schemes, credentials, blocked hosts and literal blocked IPs", async () => { await expect(assertUrlAllowed("ftp://x.com/", opts)).rejects.toMatchObject({ reason: "scheme" }); await expect(assertUrlAllowed("file:///etc/passwd", opts)).rejects.toBeInstanceOf(UrlPolicyError); await expect(assertUrlAllowed("https://user:pw@x.com/", opts)).rejects.toMatchObject({ reason: "credentials" }); await expect(assertUrlAllowed("http://localhost/", opts)).rejects.toMatchObject({ reason: "blocked_host" }); await expect(assertUrlAllowed("http://169.254.169.254/latest/meta-data", opts)).rejects.toMatchObject({ reason: "blocked_ip" }); await expect(assertUrlAllowed("http://[::ffff:127.0.0.1]/", opts)).rejects.toMatchObject({ reason: "blocked_ip" }); await expect(assertUrlAllowed("http://[64:ff9b::a00:1]/", opts)).rejects.toMatchObject({ reason: "blocked_ip" }); await expect(assertUrlAllowed("not a url", opts)).rejects.toMatchObject({ reason: "malformed" }); await expect(assertUrlAllowed("http://x.com/", { ...opts, allowHttp: false })).rejects.toMatchObject({ reason: "scheme" }); }); it("accepts public hosts and public literal IPs", async () => { await expect(assertUrlAllowed("https://www.example.com/a", opts)).resolves.toMatchObject({ hostname: "www.example.com" }); await expect(assertUrlAllowed("http://8.8.8.8/", opts)).resolves.toMatchObject({ addresses: ["8.8.8.8"] }); await expect(assertUrlAllowed("http://[2606:4700::6810:84e5]/", opts)).resolves.toMatchObject({ addresses: ["2606:4700::6810:84e5"] }); }); });