Webhooks and SSRF
Why a delivery service is an SSRF machine by construction, and how to contain it.
In short
- A webhook service makes HTTP requests to addresses supplied by its users — that is SSRF by construction.
- Resolving a name and then connecting by name allows the answer to change in between.
- HookGet resolves, validates every returned address, then connects to the exact validated address.
- A second, independent layer refuses the same traffic at the network level.
Why this is the centre of the product
Delivery means fetching a URL a customer typed. If that URL resolves to something inside the network — a metadata service, a database, an internal admin panel — the service becomes a proxy into its own infrastructure on behalf of whoever typed it. This is not an edge case; it is the normal operation of the product, pointed slightly differently.
The check that actually holds
| Step | Why it is in this order |
|---|---|
| Parse the URL strictly | Refuse credentials in the URL, non-HTTP schemes and ports outside the allowlist |
| Resolve the hostname | Get every address the name currently answers with |
| Validate every address | One private address among several is enough to refuse |
| Connect to the validated address | Not to the name — otherwise a second lookup can return something else |
| Keep the original hostname for TLS and Host | So certificate validation and virtual hosting still work |
| Never follow redirects | A redirect is the destination choosing a second address after the check |
The subtle one is step four. Validating a name and then handing the name to the HTTP client leaves a window in which DNS can answer differently — the rebinding attack. Pinning the connection to the address that was actually checked closes it.
Two layers, not one
Code can have bugs, so the network is configured to refuse the same traffic independently: the service account cannot reach private ranges or cloud metadata addresses at all. Either layer alone would be a single point of failure for the most serious class of bug in this kind of product.
If you run your own
- Validate on the resolved address, not the string.
- Check every address the name returns, including IPv6 and IPv4-mapped forms.
- Refuse redirects, or re-run the whole check on the new location.
- Do not make the exemption for local development available in production. Ours is inert when the environment says production, and there is a test that asserts it.