Webhook security fundamentals
When you expose a webhook endpoint, you create a public HTTP endpoint that accepts data from the internet. Anyone who discovers that URL can send requests to it. Without proper security measures, attackers can forge webhooks to trigger unauthorized actions, inject malicious data, or probe your system for vulnerabilities.
This article covers the essential security practices every webhook implementation needs: verifying that webhooks come from legitimate sources, protecting against replay attacks, and handling sensitive data safely.
The core problem: anyone can POST to your endpoint
Webhook URLs are not secret. They appear in configuration panels, get logged in various systems, and can be guessed or discovered through reconnaissance. An attacker who knows your endpoint URL can craft a request that looks exactly like a legitimate webhook.
Consider a payment webhook that marks orders as paid. If an attacker can send a fake "payment successful" event, they get free products. A webhook that provisions resources could be exploited to create unauthorized accounts. Even seemingly harmless webhooks might leak information about your system's behavior.
The fundamental security requirement is authentication: proving that a webhook actually came from the claimed sender. Everything else builds on this foundation.
Verifying webhooks with HMAC signatures
The standard approach to webhook authentication is HMAC (Hash-based Message Authentication Code) signatures. The webhook provider shares a secret key with you during setup. When sending a webhook, they compute a hash of the payload using this secret and include the result in a header. You compute the same hash and verify it matches.
An attacker without the secret cannot forge a valid signature. Even knowing the algorithm and seeing many valid signatures does not help because HMAC is designed to resist these attacks.
Most providers use HMAC-SHA256. The signature typically appears in a header like X-Signature, X-Hub-Signature-256, or similar. The exact format varies: some providers send just the hex-encoded hash, others prefix it with the algorithm name, and some use Base64 encoding.
Here is how verification works in practice:
import hmac
import hashlib
def verify_signature(payload, signature, secret):
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
The compare_digest function is critical. A naive string comparison like == leaks timing information that can help attackers guess the signature byte by byte. Timing-safe comparison prevents this attack.
Always verify signatures before processing any webhook data. Do not parse the JSON, access fields, or take any action until verification succeeds. Treat unverified webhooks as potentially malicious.
Preventing replay attacks with timestamps
A valid signature proves the webhook was created by someone with the secret, but it does not prove when it was created. An attacker who intercepts a legitimate webhook can resend it later. If that webhook triggers a one-time action like issuing a refund, the replay causes harm even though the signature is valid.
Timestamps solve this problem. The provider includes a timestamp in the request, often in a header or as part of the signed payload. Your verification checks that the timestamp is recent, typically within five minutes of the current time.
import time
def verify_webhook(payload, signature, timestamp, secret):
# Reject if timestamp is too old or too far in the future
current_time = int(time.time())
if abs(current_time - int(timestamp)) > 300: # 5 minutes
return False
# Verify signature (timestamp should be part of signed data)
signed_payload = f"{timestamp}.{payload}"
return verify_signature(signed_payload.encode(), signature, secret)
The timestamp must be included in the signed data. Otherwise, an attacker could replace the timestamp with a recent value while keeping the original signature. Stripe's webhook format, for example, signs the timestamp together with the payload.
Protecting your webhook secret
Your webhook secret is the key to your security. If it leaks, attackers can forge valid signatures until you rotate to a new secret.
Store the secret in your secrets manager or environment variables, never in code or version control. Restrict access so only the systems that verify webhooks can read it. Log access attempts to detect unauthorized retrieval.
When you rotate secrets, most providers support multiple active secrets during a transition period. Configure the new secret, update your verification code to accept either secret, wait for all in-flight webhooks to drain, then remove the old secret.
Additional security measures
HTTPS is mandatory for webhook endpoints. Without it, attackers on the network path can intercept the secret from the initial configuration or modify payloads in transit.
Consider IP allowlisting as an additional layer. Many providers publish the IP ranges their webhooks originate from. Blocking requests from other IPs stops attacks before they reach your verification code. However, IP lists change, so treat this as defense in depth rather than a primary control.
Validate the payload schema after verifying the signature. Malformed data, even if legitimately signed, should not crash your handler or corrupt your database. Define expected event types and field formats, and reject anything that does not match.
Rate limiting protects against denial of service. Even with valid signatures, a flood of webhooks could overwhelm your system. Apply reasonable limits based on your expected volume.
Finally, log all webhook activity. Record timestamps, event types, source IPs, and verification results. These logs help you investigate incidents, detect attacks, and debug integration issues. Avoid logging full payloads if they contain sensitive data, or redact sensitive fields before logging.
Preventing SSRF attacks (for webhook senders)
If you are building a webhook sender, you face a different threat: Server-Side Request Forgery (SSRF). When customers configure webhook URLs, they might supply addresses pointing to your internal network. Your webhook system would then make requests to internal services, potentially exposing sensitive data or triggering unintended actions.
This attack is particularly dangerous because webhook systems are designed to call arbitrary URLs. An attacker could register a webhook pointing to http://169.254.169.254/ (the cloud metadata endpoint) or http://internal-admin.local/ and receive responses meant only for internal systems.
Preventing SSRF requires blocking requests to internal networks before they happen. Use a proxy like Smokescreen that resolves DNS and filters out private IP ranges before allowing connections. Alternatively, place your webhook workers in an isolated network segment with no route to internal services.
At minimum, block requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), localhost (127.0.0.0/8), and link-local addresses (169.254.0.0/16). Be aware that attackers can use DNS rebinding to bypass naive checks, which is why resolving DNS through a filtering proxy is the more robust solution.