Webhook Security
title: "Webhook Security" description: "Learn comprehensive webhook security practices including signature verification, HTTPS enforcement, IP allowlisting, and protection against common attacks." weight: 29 date: 2026-06-28 lastmod: 2026-06-28 tags: ["apis", "webhooks"]
Webhook security is critical because webhook endpoints are publicly accessible by design. Attackers can send forged events, replay legitimate events, or probe endpoints for vulnerabilities. This lesson covers the full spectrum of security measures needed to protect both webhook providers and consumers.
## What You'll Learn
- Implement HMAC signature verification to authenticate webhook payloads
- Enforce HTTPS and TLS best practices for webhook endpoints
- Use IP allowlisting and source verification
- Protect against replay attacks, injection attacks, and denial of service
## Why It Matters
A compromised webhook endpoint can lead to unauthorized data access, fraudulent transactions, data corruption, and system compromise. Webhook security is not optional; it is the foundation of trust in event-driven integrations. A single security gap can expose your entire system to attack.
## Real-World Use
- GitHub signs webhooks with HMAC-SHA256 and recommends verifying signatures
- Stripe signs webhooks with HMAC-SHA256 and supports timestamp tolerance for replay protection
- Slack signs webhook requests with HMAC-SHA256 and provides a signing secret per app
- Shopify signs webhooks with HMAC-SHA256 and provides the raw body for signature computation
## Mermaid Flow
```mermaid
graph TD
A[Incoming Webhook] --> B{HTTPS Required?}
B -->|No| C[Reject / TLS Required]
B -->|Yes| D{Validate Signature}
D -->|Invalid| E[Reject 401]
D -->|Valid| F{Replay Check}
F -->|Replay Detected| G[Reject 401]
F -->|Fresh| H{IP Allowlisted?}
H -->|No| I[Reject 403]
H -->|Yes| J{Payload Valid?}
J -->|Malformed| K[Reject 400]
J -->|Valid| L[Process Event]
Teacher's Corner
Emphasize defense in depth: no single security measure is sufficient. Signature verification alone does not protect against replay attacks. HTTPS alone does not authenticate the sender. Combine multiple layers: TLS, signature verification, replay protection, IP allowlisting, and input validation.
Code Examples
Example 1: HMAC-SHA256 Verification with Replay Protection
import hashlib
import hmac
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_secret".encode()
TIMESTAMP_TOLERANCE = 300
def verify_webhook(payload, signature_header):
parts = signature_header.split(",")
ts = None
sigs = []
for part in parts:
if part.startswith("t="):
ts = int(part[2:])
elif part.startswith("v1="):
sigs.append(part[3:])
if ts is None or abs(time.time() - ts) > TIMESTAMP_TOLERANCE:
return False
signed_payload = f"{ts}.{payload.decode()}".encode()
expected = hmac.new(WEBHOOK_SECRET, signed_payload, hashlib.sha256).hexdigest()
for sig in sigs:
if hmac.compare_digest(expected, sig):
return True
return False
@app.route("/webhook", methods=["POST"])
def webhook():
signature = request.headers.get("X-Signature", "")
payload = request.get_data()
if not verify_webhook(payload, signature):
return jsonify({"error": "verification failed"}), 401
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=5000)
Expected Output: Valid signed payload within 5-minute window returns 200. Expired or invalid signature returns 401.
Example 2: IP Allowlisting for Known Providers
import ipaddress
from flask import Flask, request, jsonify, abort
app = Flask(__name__)
ALLOWED_NETWORKS = [
ipaddress.ip_network("192.30.252.0/22"),
ipaddress.ip_network("140.82.112.0/20"),
]
def validate_source_ip():
source_ip = request.remote_addr
for network in ALLOWED_NETWORKS:
if ipaddress.ip_address(source_ip) in network:
return True
return False
@app.route("/webhook/github", methods=["POST"])
def github_webhook():
if not validate_source_ip():
abort(403, description="Source IP not allowed")
return jsonify({"status": "ok"}), 200
@app.route("/webhook/stripe", methods=["POST"])
def stripe_webhook():
stripe_ip_ranges = ["13.112.0.0/16", "54.0.0.0/16"]
source_ip = request.remote_addr
for network_str in stripe_ip_ranges:
if ipaddress.ip_address(source_ip) in ipaddress.ip_network(network_str):
return jsonify({"status": "ok"}), 200
abort(403, description="Source IP not allowed")
if __name__ == "__main__":
app.run(port=5000)
Expected Output: Requests from allowed IP ranges pass through. Requests from other IPs receive 403.
Example 3: Input Validation and Sanitization
import json
import re
from flask import Flask, request, jsonify
app = Flask(__name__)
def sanitize_event_id(event_id):
return re.sub(r'[^a-zA-Z0-9\-_.]', '', event_id)
def validate_payload(payload):
if not isinstance(payload, dict):
return False, "payload must be a JSON object"
if "id" not in payload:
return False, "missing required field: id"
if "type" not in payload:
return False, "missing required field: type"
if len(json.dumps(payload)) > 1024 * 100:
return False, "payload exceeds 100KB limit"
return True, None
@app.route("/webhook", methods=["POST"])
def webhook():
payload = request.get_json(silent=True)
if payload is None:
return jsonify({"error": "invalid JSON"}), 400
valid, error = validate_payload(payload)
if not valid:
return jsonify({"error": error}), 400
payload["id"] = sanitize_event_id(payload.get("id", ""))
print(f"Processing {payload['type']}: {payload['id']}")
return jsonify({"status": "ok"}), 200
if __name__ == "__main__":
app.run(port=5000)
Expected Output: Malformed or missing fields return 400 with specific error messages. Valid payloads process normally.
Common Mistakes
- Using string comparison for HMAC verification instead of
hmac.compare_digestorcrypto.timingSafeEqual - Not validating the timestamp, allowing replay of captured webhook payloads
- Accepting HTTP connections instead of enforcing HTTPS at the infrastructure level
- Storing webhook secrets in version control or configuration files without encryption
- Not validating payload size before processing, risking memory exhaustion
- Logging full webhook payloads containing sensitive data (PII, API keys, credit card numbers)
- Assuming IP allowlisting alone is sufficient security without signature verification
Practice Questions
- Why is timestamp-based replay protection important even with signature verification?
- What is the purpose of
hmac.compare_digestand how does it prevent timing attacks? - Why should webhook payloads be validated even after signature verification?
- How would you rotate webhook secrets without disrupting active integrations?
- Challenge: Design a webhook security scheme that supports multiple signing keys (for key rotation), timestamp tolerance with configurable window, event-level idempotency tracking, payload encryption for sensitive fields, and audit logging of all verification outcomes.
Answer Key
1. Signature verification proves the payload came from the right source, but an attacker can capture a valid signed payload and replay it later. Timestamp checking limits the replay window. 2. `compare_digest` takes constant time regardless of how many characters match. A naive `==` comparison short-circuits on the first mismatch, leaking information about the correct signature byte-by-byte. 3. Signature verification only authenticates the sender. The payload may still contain malformed, malicious, or unexpected data. Input validation prevents injection attacks and data corruption. 4. Maintain a list of active keys. Sign new webhooks with the new key but accept verification against any key in the list. Remove old keys after a transition period. Provide a webhook test event to verify the new key works. 5. Use a key ID header to identify which key signed the payload, maintain a rotating key store, include `t=timestamp` in the signature payload, use event IDs with a deduplication cache, encrypt sensitive payload fields with the consumer's public key, and log all verification outcomes with event ID, key ID, timestamp, and result.FAQ
Mini Project
Build a secure webhook gateway in Python. Create a reverse proxy that: (1) terminates TLS, (2) validates HMAC-SHA256 signatures with timestamp replay protection, (3) supports multiple provider IP allowlists, (4) sanitizes and validates payload fields, (5) logs all verification attempts (success, failure, replay) without logging sensitive payload data, (6) exposes Prometheus metrics for verification success/failure counts, and (7) provides a /health endpoint for readiness checks.
What's Next
Put everything together in the webhook project, where you will build a complete webhook system combining provider, consumer, and security patterns.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro