SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
3 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
4.9 KB · 72 lines typescript
Raw Blame History
1import { describe, expect, it } from "vitest";2import { assertUrlAllowed, isBlockedHostname, isBlockedIP, isBlockedIPv4, isBlockedIPv6, UrlPolicyError } from "./ssrf.js";34describe("blocked hostnames", () => {5  it("blocks loopback / metadata / internal suffixes / bare labels", () => {6    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);7  });8  it("allows public hosts", () => {9    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);10  });11});1213describe("blocked IPv4 ranges", () => {14  it("private, loopback, link-local, CGNAT, multicast, reserved, documentation", () => {15    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);16  });17  it("public addresses pass, including neighbours of blocked ranges", () => {18    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);19  });20});2122describe("blocked IPv6", () => {23  it("loopback, unspecified, link-local, unique-local, multicast, documentation", () => {24    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);25    expect(isBlockedIPv6("2606:4700::6810:84e5")).toBe(false);26    expect(isBlockedIPv6("2001:4860:4860::8888")).toBe(false);27  });28  it("IPv4-mapped addresses follow the IPv4 policy (dotted and hex forms)", () => {29    expect(isBlockedIPv6("::ffff:127.0.0.1")).toBe(true);30    expect(isBlockedIPv6("::ffff:10.1.2.3")).toBe(true);31    expect(isBlockedIPv6("::ffff:8.8.8.8")).toBe(false);32    expect(isBlockedIPv6("::ffff:7f00:1")).toBe(true); // 127.0.0.133    expect(isBlockedIPv6("::ffff:a9fe:a9fe")).toBe(true); // 169.254.169.25434    expect(isBlockedIPv6("::ffff:808:808")).toBe(false); // 8.8.8.835    expect(isBlockedIPv6("0:0:0:0:0:ffff:c0a8:1")).toBe(true); // 192.168.0.136  });37  it("NAT64 well-known and local-use prefixes embed IPv4 and follow the IPv4 policy", () => {38    expect(isBlockedIPv6("64:ff9b::808:808")).toBe(false); // 8.8.8.8 via NAT64 stays reachable39    expect(isBlockedIPv6("64:ff9b::a00:1")).toBe(true); // 10.0.0.140    expect(isBlockedIPv6("64:ff9b::7f00:1")).toBe(true); // 127.0.0.141    expect(isBlockedIPv6("64:ff9b:1:0:0:0:a9fe:a9fe")).toBe(true); // local-use prefix, 169.254.169.25442    expect(isBlockedIPv6("64:ff9b:1:0:0:0:808:808")).toBe(false);43    expect(isBlockedIPv6("64:ff9b:dead::")).toBe(true); // malformed NAT64 form is blocked44  });45  it("isBlockedIP dispatches by family and rejects non-IPs", () => {46    expect(isBlockedIP("10.0.0.1")).toBe(true);47    expect(isBlockedIP("::1")).toBe(true);48    expect(isBlockedIP("not-an-ip")).toBe(true);49    expect(isBlockedIP("9.9.9.9")).toBe(false);50  });51});5253describe("assertUrlAllowed (no DNS)", () => {54  const opts = { resolve: false };55  it("rejects bad schemes, credentials, blocked hosts and literal blocked IPs", async () => {56    await expect(assertUrlAllowed("ftp://x.com/", opts)).rejects.toMatchObject({ reason: "scheme" });57    await expect(assertUrlAllowed("file:///etc/passwd", opts)).rejects.toBeInstanceOf(UrlPolicyError);58    await expect(assertUrlAllowed("https://user:pw@x.com/", opts)).rejects.toMatchObject({ reason: "credentials" });59    await expect(assertUrlAllowed("http://localhost/", opts)).rejects.toMatchObject({ reason: "blocked_host" });60    await expect(assertUrlAllowed("http://169.254.169.254/latest/meta-data", opts)).rejects.toMatchObject({ reason: "blocked_ip" });61    await expect(assertUrlAllowed("http://[::ffff:127.0.0.1]/", opts)).rejects.toMatchObject({ reason: "blocked_ip" });62    await expect(assertUrlAllowed("http://[64:ff9b::a00:1]/", opts)).rejects.toMatchObject({ reason: "blocked_ip" });63    await expect(assertUrlAllowed("not a url", opts)).rejects.toMatchObject({ reason: "malformed" });64    await expect(assertUrlAllowed("http://x.com/", { ...opts, allowHttp: false })).rejects.toMatchObject({ reason: "scheme" });65  });66  it("accepts public hosts and public literal IPs", async () => {67    await expect(assertUrlAllowed("https://www.example.com/a", opts)).resolves.toMatchObject({ hostname: "www.example.com" });68    await expect(assertUrlAllowed("http://8.8.8.8/", opts)).resolves.toMatchObject({ addresses: ["8.8.8.8"] });69    await expect(assertUrlAllowed("http://[2606:4700::6810:84e5]/", opts)).resolves.toMatchObject({ addresses: ["2606:4700::6810:84e5"] });70  });71});72