graph.facebook.com URL) and ones the merchant controls (webhook endpoints). The second kind is the dangerous one, and it gets a dedicated guard.
The threat
A webhook URL is typed by the merchant, and the delivery’s response body is persisted and rendered in the dashboard’s delivery inspector. That combination is a classic read-SSRF: The fetch originates from inside the trust boundary, so it can reach internal services and cloud metadata that the merchant never could directly — and then read the response back out.The guard, in two layers
WebhookUrlGuard blocks this at creation and at delivery, because a single check is insufficient against DNS rebinding.
Layer 1 — format validation at write time
isFormatSafe(url) rejects anything that isn’t a public https endpoint:
- protocol must be
https - host must not be
localhost,*.localhost,*.internal,*.local, ormetadata.google.internal - if the host is an IP literal, it must not be private/reserved
| Range | Why it’s blocked |
|---|---|
10/8, 172.16/12, 192.168/16 | RFC-1918 private |
127/8, ::1 | loopback |
169.254/16 | link-local + cloud metadata |
100.64/10 | CGNAT |
198.18/15 | benchmarking |
0/8, 255/8 | unspecified / broadcast |
fc00::/7, fe80::/10 | IPv6 ULA + link-local |
::ffff:0:0/96 | IPv4-mapped (extract and re-check the v4) |
Layer 2 — DNS resolution at delivery time
A hostname that passed Layer 1 can still resolve to a private IP — that’s DNS rebinding: registerevil.com, point it at a public IP to pass creation, then flip it to 169.254.169.254 before delivery. So at delivery, assertResolvable(url) resolves the host and rejects if any returned address is private. A blocked delivery fails terminally with blocked destination and is not retried.
Outbound we control is fixed
The WhatsApp client only ever callshttps://graph.facebook.com/{version}/{phoneNumberId}/messages — a constant URL with no user-controlled host — so it carries no SSRF surface. The guard exists specifically for the merchant-controlled case.
The guard is unit-tested across the whole matrix above (https/non-https, loopback, metadata, private ranges, bracketed IPv6, garbage input). It’s the concrete answer to “what stops a malicious merchant from turning your webhook system into a credential reader.”
That’s the system. Jump to the API Reference to start firing requests.