Most Webhook Endpoints Are Trivially Spoofable. Here's the Validation You Need.
Cybersecurity
Webhooks are the unguarded back door of modern SaaS. Without HMAC signatures, replay protection, and source verification, attackers can forge payments, trigger workflows, and bypass your auth model entirely. The fix is two days of work.
By Arjun Raghavan, Security & Systems Lead, BIPI · January 28, 2024 · 6 min read
An e-commerce platform we pen-tested in late 2023 had a webhook endpoint for payment confirmations. The endpoint took a JSON body containing order_id, status, and amount. It verified nothing. We sent a POST with status=paid for a $10,000 order without ever paying. The order shipped. Total bounty: writing one curl command.
Webhook security is consistently treated as an afterthought because the path looks innocuous. The endpoint accepts a POST, the body looks structured, the integration works. Engineers ship and move on. Meanwhile every webhook URL is a publicly-reachable HTTP endpoint that, if unauthenticated, lets an attacker forge whatever state transitions the application allows.
What Real Validation Looks Like
There are four layers. Each one mitigates a different attack. Skip any of them and you've left a hole.
- HMAC signature over the raw request body using a shared secret
- Replay protection via a timestamp in the signed material and a tight skew window
- Idempotency token to prevent legitimate double-processing
- Source IP allowlist when the sender publishes their egress ranges
HMAC Signature Mechanics
The sender computes HMAC-SHA256(secret, body) and includes it in a header like X-Hub-Signature-256 (GitHub's convention) or Stripe-Signature. The receiver re-computes the HMAC over the raw bytes of the body and compares with constant-time equality. Two implementation traps catch most teams.
First, you must hash the raw body, not the parsed JSON. If your framework parses the body before your handler sees it, the bytes you re-serialize won't match what the sender hashed. In Express, use express.raw() or capture the body in a middleware. In Django REST Framework, override the request body access in a custom parser. In Go net/http, read req.Body before json.NewDecoder.
Second, comparison must be constant-time. crypto.timingSafeEqual in Node, hmac.compare_digest in Python, subtle.ConstantTimeCompare in Go. Plain string equality leaks timing information that lets an attacker iterate signatures byte by byte until they hit the right one.
Replay Protection
HMAC alone doesn't prevent replay. An attacker who captures one legitimate webhook can re-send it indefinitely. The fix is including a timestamp in the signed payload (Stripe puts it in the Stripe-Signature header) and rejecting timestamps outside a tight window, 5 minutes is typical.
Better, store a nonce or event_id and reject duplicates. This both prevents replay and handles the case where the sender legitimately retries because they didn't get a 200. Idempotency keys are not just a security control; they save you from double-charging cards or double-shipping orders during retry storms.
Source IP Allowlists
Stripe, GitHub, Shopify, Twilio, and most major SaaS publish their webhook egress IP ranges. Pin your webhook endpoint behind a firewall or WAF rule that only accepts those ranges. This is defense in depth, even with HMAC, an attacker with a leaked secret can't forge requests if they can't reach the endpoint from a trusted IP.
The catch is that IP ranges change. Subscribe to the vendor's announcements (most publish via RSS or status page) and automate the firewall update. We've seen multiple production outages caused by webhook senders moving to new IP ranges silently. A monthly job that queries the vendor's published ranges and updates the WAF rule is the right pattern.
Common Failure Modes
- Validating signature against parsed/re-serialized body instead of raw bytes
- Using == instead of constant-time comparison
- No replay window enforcement, accepting any valid signature regardless of age
- Logging webhook bodies including secrets to application logs
- Storing webhook secrets in source code or environment variables checked into git
- Skipping validation in non-production environments, until prod config drift introduces the gap
Build It Once, Reuse It
Most companies have 5-20 webhook integrations. Build a shared middleware that handles HMAC, replay window, and idempotency uniformly. Each integration provides the secret and signature header name; the middleware handles everything else. This eliminates the most common failure mode: one integration is rigorous, another was shipped at 5pm on Friday and forgot validation entirely.
Read more field notes, explore our services, or get in touch at info@bipi.in. Privacy Policy · Terms.