118 lines
4.0 KiB
TypeScript
118 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
assertAddressAllowed,
|
|
assertSafeWebhookUrl,
|
|
UnsafeWebhookUrlError,
|
|
} from "../app/v3/services/alerts/safeWebhookUrl.server.js";
|
|
|
|
// These cases are decided by the lexical / IP-literal checks, so no network is
|
|
// needed. The DNS-resolution branch is not exercised here.
|
|
describe("assertSafeWebhookUrl", () => {
|
|
it("accepts a public http(s) URL given as an IP literal (no DNS needed)", async () => {
|
|
await expect(assertSafeWebhookUrl("https://93.184.216.34/hook?x=1")).resolves.toBeInstanceOf(
|
|
URL
|
|
);
|
|
});
|
|
|
|
it("rejects non-http(s) schemes", async () => {
|
|
for (const url of [
|
|
"file:///etc/passwd",
|
|
"gopher://evil/_",
|
|
"ftp://host/x",
|
|
"data:text/plain,hi",
|
|
"javascript:alert(1)",
|
|
]) {
|
|
await expect(assertSafeWebhookUrl(url)).rejects.toBeInstanceOf(UnsafeWebhookUrlError);
|
|
}
|
|
});
|
|
|
|
it("rejects loopback and unspecified IPv4", async () => {
|
|
for (const host of ["127.0.0.1", "127.9.9.9", "0.0.0.0"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects RFC1918 private ranges", async () => {
|
|
for (const host of ["10.0.0.1", "172.16.0.1", "172.31.255.1", "192.168.1.1"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects link-local incl. the cloud metadata address", async () => {
|
|
await expect(
|
|
assertSafeWebhookUrl("http://169.254.169.254/latest/meta-data/")
|
|
).rejects.toBeInstanceOf(UnsafeWebhookUrlError);
|
|
});
|
|
|
|
it("rejects CGNAT, multicast and reserved ranges", async () => {
|
|
for (const host of ["100.64.0.1", "224.0.0.1", "239.1.1.1", "240.0.0.1"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects loopback/internal hostnames before any DNS lookup", async () => {
|
|
for (const host of ["localhost", "foo.localhost", "svc.internal", "db.local"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects unsafe IPv6 literals", async () => {
|
|
for (const host of ["[::1]", "[fe80::1]", "[fc00::1]", "[::ffff:127.0.0.1]"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
// Regression: bracketed IPv6 literals and Node's hex-normalized IPv4-mapped
|
|
// addresses (::ffff:127.0.0.1 -> ::ffff:7f00:1) must be caught lexically.
|
|
it("rejects bracketed and hex-mapped IPv6 loopback lexically", async () => {
|
|
for (const host of ["[::1]", "[::ffff:7f00:1]"]) {
|
|
await expect(assertSafeWebhookUrl(`http://${host}/hook`)).rejects.toBeInstanceOf(
|
|
UnsafeWebhookUrlError
|
|
);
|
|
}
|
|
});
|
|
|
|
it("rejects malformed URLs", async () => {
|
|
await expect(assertSafeWebhookUrl("not a url")).rejects.toBeInstanceOf(UnsafeWebhookUrlError);
|
|
});
|
|
});
|
|
|
|
// assertAddressAllowed is the connect-time check that safeWebhookFetch runs
|
|
// inside the socket's DNS lookup. These cases cover its address-range logic.
|
|
describe("assertAddressAllowed", () => {
|
|
it("allows public IPv4 / IPv6 addresses", () => {
|
|
expect(() => assertAddressAllowed("93.184.216.34", 4)).not.toThrow();
|
|
expect(() => assertAddressAllowed("2606:2800:220:1:248:1893:25c8:1946", 6)).not.toThrow();
|
|
});
|
|
|
|
it("rejects loopback / private / CGNAT / link-local IPv4 (incl. metadata)", () => {
|
|
for (const addr of [
|
|
"127.0.0.1",
|
|
"0.0.0.0",
|
|
"10.1.2.3",
|
|
"172.16.0.1",
|
|
"192.168.1.1",
|
|
"169.254.169.254",
|
|
"100.64.0.1",
|
|
]) {
|
|
expect(() => assertAddressAllowed(addr, 4)).toThrow(UnsafeWebhookUrlError);
|
|
}
|
|
});
|
|
|
|
it("rejects loopback / ULA / link-local / mapped IPv6", () => {
|
|
for (const addr of ["::1", "fe80::1", "fc00::1", "::ffff:127.0.0.1"]) {
|
|
expect(() => assertAddressAllowed(addr, 6)).toThrow(UnsafeWebhookUrlError);
|
|
}
|
|
});
|
|
});
|