fix(backend): close two SSRF bypasses in webhook URL validation

1. The URL guard only classified IP-literal hostnames; any plain domain
   name (the common case) skipped validation entirely. Hostnames are now
   resolved via DNS and every returned address is checked against the
   same private/loopback/link-local ranges (net.BlockList), so an
   attacker-controlled domain pointed at an internal IP is rejected too.
2. fetch() follows redirects by default, so a registered public URL could
   302 to an internal target at delivery time. Delivery now uses
   redirect: 'manual' so such a redirect is never followed.
This commit is contained in:
Nick Szaunig committed 2026-08-23 11:08:40 +02:00
1 parent a0f7573af5
commit 79010a926b
4 files changed
+157 -71

No files matched your search

@@ -45,7 +45,7 @@ describe('WebhooksService', () => {
);
const subscription = await service.create(
{ url: 'https://example.test/hook', events: ['colony.created'] },
{ url: 'https://203.0.113.10/hook', events: ['colony.created'] },
'user-1',
);
@@ -53,7 +53,7 @@ describe('WebhooksService', () => {
const [[createArg]] = prisma.webhookSubscription.create.mock.calls as [
[{ data: { url: string; events: string[]; createdByUserId: string } }],
];
expect(createArg.data.url).toBe('https://example.test/hook');
expect(createArg.data.url).toBe('https://203.0.113.10/hook');
expect(createArg.data.events).toEqual(['colony.created']);
expect(createArg.data.createdByUserId).toBe('user-1');
});