· 2 min read
SSRF in webhooks: checking the address after DNS
Why a webhook delivery service checks the addresses after DNS resolution, not the URL string, and how hook-relay connects to the very address it checked.
A webhook service by definition calls addresses a user gave it. So it can be asked to go to http://127.0.0.1, an internal network or cloud metadata. That is SSRF.
Checking the URL string is useless
The name evil.example may point to 127.0.0.1, and a public host may return both a public and a private address. So hook-relay checks the DNS result, not the text.
What hook-relay does
- It accepts only
httpandhttps, with no login or password in the URL. - It resolves the host through DNS and requires all returned addresses to be public. Loopback, private ranges, link-local, CGNAT, documentation, ULA, IPv4-mapped, NAT64 and 6to4 with an embedded private address are blocked.
- The check runs at receiver registration and again on every delivery.
- The connection goes to exactly the checked address (in the code,
resolve(host, addr)on the HTTP client), so DNS cannot be swapped between the check and the connection: protection from DNS rebinding.
How it is tested
Proptest: any private IPv4 and its IPv4-mapped form are blocked. Integration tests cover SSRF at registration and at delivery. The tests themselves need a receiver on loopback, hence the variable HOOK_RELAY_ALLOW_PRIVATE_FOR_TESTS=1: it switches the protection off, a warning is printed to the log, and it must not be enabled anywhere but tests.
Limitations
The receiver response body is not read or stored; delivery is at-least-once, so the receiver must be idempotent. The service is not deployed to the server yet.