· 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.

Project: hook-relay

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

  1. It accepts only http and https, with no login or password in the URL.
  2. 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.
  3. The check runs at receiver registration and again on every delivery.
  4. 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.