Designing API Webhook Recipient Allowlisting: Defending Against SSRF and Internal Network Exposure
Every webhook product accepts a customer-supplied URL and makes HTTP requests to it from infrastructure the customer does not control. The default behavior is server-side request forgery as a product feature. Recipient allowlisting is the discipline that closes the gap.
The basic webhook product accepts a customer-supplied URL and calls it when events happen. The product is one server-side HTTP client away from being a server-side request forgery vector. The customer can supply any URL the product's HTTP client can resolve and connect to. From the perspective of the infrastructure the product runs on, that means cloud metadata endpoints at 169.254.169.254, internal services on 10.0.0.0/8 and 172.16.0.0/12 and 192.168.0.0/16, the loopback interface at 127.0.0.0/8 and ::1, and any host the product's DNS can reach.
The threat surface is concrete. AWS instance metadata endpoints return temporary credentials when called from inside the instance. Kubernetes cluster-internal services return data that should never leave the cluster. Self-hosted infrastructure exposes admin endpoints that assume the only callers are trusted. A webhook product that calls customer-supplied URLs without validation can be used to enumerate the product's own infrastructure, exfiltrate internal data, and obtain credentials from cloud metadata services.
The recipient allowlisting discipline closes the gap. It is not glamorous and it does not add features customers ask for, but it converts the product from a security liability into a security boundary.
What the URL validation should actually check
URL syntax validation alone catches none of the SSRF attacks. The attack URLs are all syntactically valid. The validation that matters is at the IP layer after DNS resolution, and it has to happen at connection time not at subscription time because DNS responses can change.
The four-category check covers most attacks. Resolved IP must be a publicly routable address — not loopback, not link-local, not multicast, not RFC 1918 private space, not cloud metadata endpoints, not the product's own infrastructure CIDRs. The port must be in the permitted set — 80 and 443 for most products. The scheme must be https for production webhooks with the test sandbox optionally accepting http. The hostname must not be a string that resolves to multiple A records where at least one is private — the DNS rebinding attack works by serving public IPs at validation time and private IPs at connection time.
The DNS rebinding subtlety
DNS rebinding is the SSRF attack that survives basic IP validation. The attacker controls a domain like evil.example.com with a short TTL and an A record alternating between a public IP and 169.254.169.254. The webhook product validates the URL at subscription time and resolves to the public IP. The product then makes the actual HTTP request seconds or minutes later, and the next DNS lookup returns the metadata endpoint IP. The validation passes and the attack succeeds.
The defense is resolving the hostname once at the validation or connection step and pinning the connection to that resolved IP for the duration of the request. The Python requests library exposes this through a custom adapter. The Node node:http library exposes it through the lookup option on the request. The Go net/http library exposes it through a custom Dialer. Every HTTP library has a way to pin DNS at the connection layer, and the webhook product needs to use it because the alternative is leaving the rebinding window open.
The customer-bypass tier
Some legitimate customer setups need localhost callbacks during testing — a developer running a webhook receiver on their laptop with a tunnel like ngrok or Cloudflare Tunnel, a local CI runner receiving webhooks from a sandbox environment, a customer with a corporate VPN exposing internal services via a tunnel. The default-deny posture blocks these as it should, but the test sandbox needs a path that allows them.
The pattern that works: production API keys reject any URL that does not pass the four-category check. Test API keys accept localhost and RFC 1918 addresses for documented testing scenarios, with the documentation explicit that the test API keys can only deliver test events. The split is enforced at the API key level, not the URL level, so a customer cannot accidentally configure a production subscription pointing at localhost.
The CIDR exclusion list
The four-category check needs an explicit list of excluded CIDRs, not just RFC 1918 ranges. The minimum list includes 0.0.0.0/8 (current network), 10.0.0.0/8 (RFC 1918), 100.64.0.0/10 (carrier-grade NAT), 127.0.0.0/8 (loopback), 169.254.0.0/16 (link-local plus AWS metadata), 172.16.0.0/12 (RFC 1918), 192.0.0.0/24 (reserved), 192.168.0.0/16 (RFC 1918), 198.18.0.0/15 (benchmarking), and 224.0.0.0/4 (multicast). The IPv6 equivalents include ::1 (loopback), fe80::/10 (link-local), fc00::/7 (unique local), and the IPv4-mapped IPv6 range that lets attackers bypass IPv4-only checks.
The product-specific addition is the CIDRs of the product's own infrastructure. If the product runs in a VPC with 10.20.0.0/16 internal addressing, that range needs to be in the exclusion list explicitly. The cloud provider metadata endpoint specifically also needs an entry — 169.254.169.254 for AWS and most clouds, with cloud-specific variants for some providers.
The redirect handling
The 3xx redirect handling is the second category of SSRF. The webhook product calls a public URL that returns a 302 redirect to a private URL. The HTTP client follows the redirect by default and ends up making the request to the private URL with full credentials and headers. The defense is either disabling redirect following entirely for webhook delivery or applying the same four-category check to every redirect target before following.
The right default is no automatic redirect following for webhook delivery. The webhook contract is that the customer-configured URL is the endpoint that receives the event. Following redirects to other URLs is silently changing the contract, and the operational benefit is small enough that the security cost is not worth it.
What allowlisting does not prevent
Allowlisting closes the SSRF attack surface. It does not close the other webhook attack surfaces. A customer-configured URL that the product calls is still an attack vector if the URL belongs to a third party the customer does not actually control. The customer's product enumeration through webhook timing is still possible — the product calls URL A and not URL B, and the attacker learns something about which events fire when. The data exfiltration through legitimate customer endpoints is still possible — the customer is the one receiving the payloads and the customer's logging may include the data.
The webhook product cannot solve these without changing what a webhook is. What it can do is prevent its own infrastructure from being used as an SSRF vector against itself or against other customers. That is what allowlisting accomplishes, and the discipline is more about closing a known security gap than about adding a feature customers see.
Our use across the four products
WebhookVault is the product most affected because it captures and replays HTTP requests with full body and header preservation. The allowlisting applies to both the capture URL and the replay target. The validation happens at subscription time and again at every connection, with DNS pinned between resolution and connection. The replay target validation is stricter than the original capture because the replay is an outbound request the product initiates.
CronPing and FlagBit and DocuMint each make outbound HTTP requests for webhook notifications, ping callbacks, and Stripe webhooks. Each has the same shared allowlisting library that enforces the four-category check and DNS pinning. The shared library means a security fix in one place applies to all four products simultaneously, and a single audit of the library covers the SSRF defense for the entire studio.
The deeper observation is that webhook products are SSRF-by-default unless the product team deliberately closes the gap. The default behavior of any HTTP client is to call any URL it is given. The default behavior of DNS is to return whatever the authoritative server says. The default behavior of the OS is to route packets to whatever destination the connection requests. The webhook product has to override all three defaults to be safe, and the override is not automatic. The teams that have been bitten by SSRF do this; the teams that have not been bitten yet often do not.
Our products: DocuMint (PDF invoice generation API), CronPing (cron job monitoring with status pages), FlagBit (feature flags API for modern teams), and WebhookVault (webhook capture and replay) put these patterns into production.