Verifying Webhook Signatures
An attacker found our webhook URL in a client-side error log and POSTed a fake order.completed event. Our handler processed it — no signature check, no authentication. One line of middleware verifying HMAC-SHA256 would have rejected it. We added signature verification that day and rotated the webhook secret as a precaution.
HMAC signature scheme
Most providers use HMAC-SHA256:
signature = HMAC-SHA256(secret, timestamp + "." + raw_body)
The sender includes the signature and timestamp in headers. The receiver recomputes and compares.
Stripe-style verification
import hmac
import hashlib
import time
def verify_stripe_signature(payload: bytes, sig_header: str, secret: str, tolerance: int = 300):
elements = dict(item.split("=", 1) for item in sig_header.split(","))
timestamp = int(elements["t"])
received_sig = elements["v1"]
# Reject stale timestamps
if abs(time.time() - timestamp) > tolerance:
raise WebhookError("Timestamp outside tolerance window")
# Compute expected signature
signed_payload = f"{timestamp}.".encode() + payload
expected_sig = hmac.new(
secret.encode(), signed_payload, hashlib.sha256
).hexdigest()
# Constant-time comparison
if not hmac.compare_digest(expected_sig, received_sig):
raise WebhookError("Invalid signature")
return True
Generic HMAC verification
For custom webhooks or providers using simpler schemes:
def verify_hmac_signature(payload: bytes, signature: str, secret: str):
expected = hmac.new(
secret.encode(), payload, hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
Header format varies by provider:
| Provider | Header | Format |
|---|---|---|
| Stripe | Stripe-Signature |
t=timestamp,v1=sig |
| GitHub | X-Hub-Signature-256 |
sha256=hex_sig |
| Shopify | X-Shopify-Hmac-Sha256 |
base64 sig |
| Svix | svix-signature |
v1,base64_sig |
Express middleware
function webhookVerifier(secret) {
return (req, res, next) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const rawBody = req.rawBody; // capture before JSON parsing
if (!signature || !timestamp) {
return res.status(401).json({ error: 'Missing signature headers' });
}
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
next();
};
}
// Capture raw body before JSON middleware
app.use('/webhooks', express.raw({ type: 'application/json' }));
app.use('/webhooks', webhookVerifier(process.env.WEBHOOK_SECRET));
app.use('/webhooks', (req, res, next) => {
req.body = JSON.parse(req.body);
next();
});
Raw body capture
Framework-specific raw body access:
# FastAPI
@app.post("/webhooks")
async def handle_webhook(request: Request):
body = await request.body() # raw bytes
signature = request.headers.get("X-Webhook-Signature")
verify_hmac(body, signature, SECRET)
payload = json.loads(body)
# Rails — skip params parsing for webhook routes
class WebhooksController < ApplicationController
skip_before_action :verify_authenticity_token
def create
payload = request.raw_post
verify_signature(payload, request.headers["X-Signature"])
process(JSON.parse(payload))
end
end
Secret management
- Generate secrets with
openssl rand -hex 32 - Store in a secrets manager, not environment variables in code
- Support secret rotation with dual-secret verification during transition
- Rotate immediately if a secret is exposed
- Use different secrets per webhook endpoint
def verify_with_rotation(payload, signature, timestamp):
for secret in [current_secret, previous_secret]:
try:
return verify_signature(payload, signature, secret, timestamp)
except WebhookError:
continue
raise WebhookError("No valid secret matched")
Security checklist
- Verify signature on every request — no exceptions for "testing"
- Use raw body bytes — never re-serialize parsed JSON
- Validate timestamp — reject replays outside tolerance window
- Constant-time comparison — use
hmac.compare_digestorcrypto.timingSafeEqual - Return generic 401 — don't leak whether timestamp or signature failed
- Log verification failures — alert on repeated failures from same IP
- Rotate secrets — support dual-secret during rotation windows
Clock skew tolerance
Set timestamp tolerance to 5 minutes (300 seconds) — enough for minor clock drift, short enough to limit replay windows. Log rejected timestamps with the delta for debugging NTP issues on receiver servers.
Testing signature verification
Generate test vectors in CI:
def test_signature_roundtrip():
payload = b'{"event": "test"}'
ts = str(int(time.time()))
sig = compute_signature(payload, ts, TEST_SECRET)
assert verify(payload, f"t={ts},v1={sig}", TEST_SECRET)
Resources
- Stripe webhook signatures
- GitHub webhook validation
- Svix webhook verification library
- OWASP Webhook Security
- Shopify HMAC verification
Operational checklist (1)
Before promoting Webhooks Signature Verification changes, confirm observability dashboards cover error rate and p75 latency for affected routes, rollback is documented in the pull request, and a staging drill reproduced the last known failure mode.
Field validation (2)
Re-baseline Webhooks Signature Verification after browser upgrades or CDN configuration changes. Mobile share above seventy percent shifts median device class — optimizations tuned on desktop lab profiles may not transfer.
Coordination (3)
Align with platform and backend owners on cache TTL, deploy windows, and API contracts when Webhooks Signature Verification touches shared infrastructure — single-layer wins often disappear when another tier invalidates caches.
Operational checklist (4)
Before promoting Webhooks Signature Verification changes, confirm observability dashboards cover error rate and p75 latency for affected routes, rollback is documented in the pull request, and a staging drill reproduced the last known failure mode.
Field validation (5)
Re-baseline Webhooks Signature Verification after browser upgrades or CDN configuration changes. Mobile share above seventy percent shifts median device class — optimizations tuned on desktop lab profiles may not transfer.
Coordination (6)
Align with platform and backend owners on cache TTL, deploy windows, and API contracts when Webhooks Signature Verification touches shared infrastructure — single-layer wins often disappear when another tier invalidates caches.
Cross-team contracts for webhooks signature verification
Document producers, consumers, timeouts, and idempotency keys. Silent schema or policy changes are how webhooks signature verification breaks without a clear owner in the incident channel.
| Check | Expected for webhooks signature verification |
|---|---|
| Happy path | Pass |
| Injected fault | Controlled degradation |
| After rollback | Prior stable behavior |
Concrete probe 1: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Capacity and cost notes for webhooks signature verification
Estimate QPS, payload size, cardinality, and downstream saturation. Functionally correct webhooks signature verification changes still cause outages through pool exhaustion, crawl waste, or CPU amplification.
Concrete probe 2: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Reviewer checklist for webhooks signature verification
Ask what happens when the dependency is slow, when authz is skipped on batch jobs, and when clients retry. Those three questions catch most webhooks signature verification regressions before production.
| Check | Expected for webhooks signature verification |
|---|---|
| Happy path | Pass |
| Injected fault | Controlled degradation |
| After rollback | Prior stable behavior |
Concrete probe 3: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Incident patterns around webhooks signature verification
Most incidents involving webhooks signature verification start as a silent drift: a secondary path skips the control, a retry amplifies load, or a config default from a tutorial ships to production. Write the failure story before the happy path.
Concrete probe 4: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Invariants to enforce for webhooks signature verification
Name three invariants that must hold after every deploy of webhooks signature verification. Encode at least one in an automated test that fails when the invariant is disabled. Reviewers should reject PRs that only cover the primary UI path.
| Check | Expected for webhooks signature verification |
|---|---|
| Happy path | Pass |
| Injected fault | Controlled degradation |
| After rollback | Prior stable behavior |
Concrete probe 5: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Telemetry and ownership for webhooks signature verification
Pair a leading operational signal with a lagging user or risk outcome. Page on burn related to webhooks signature verification, not vanity counters. Keep a named owner and a dashboard link in the service catalog entry.
Concrete probe 6: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Rollout sequence for webhooks signature verification
Prefer flags, weighted routes, or dual-running configs. Rehearse rollback once in staging. The on-call note for webhooks signature verification should include the revert command and the expected user-visible effect within five minutes.
| Check | Expected for webhooks signature verification |
|---|---|
| Happy path | Pass |
| Injected fault | Controlled degradation |
| After rollback | Prior stable behavior |
Concrete probe 7: inject the failure mode you fear for webhooks signature verification in staging, confirm the alarm fires, and confirm users see a controlled fallback. Record the result in the change ticket so the next on-call is not guessing.
Frequently asked questions
Why is webhook signature verification necessary?
Webhook endpoints are public URLs. Without signature verification, anyone who discovers the URL can send fake events — creating fraudulent orders, triggering refunds, or modifying account state. Signatures prove the payload came from the expected sender and wasn't modified in transit. Always verify before processing any webhook payload.
Should I verify the signature before or after parsing the JSON body?
Verify before parsing. Read the raw request body as bytes, compute the expected signature, and compare. Then parse JSON. If you parse first and re-serialize, whitespace and key ordering differences will cause signature mismatches. Most frameworks let you access the raw body before JSON middleware processes it.
How do timestamp checks prevent replay attacks?
Include a timestamp in the signed payload. Reject webhooks with timestamps older than your tolerance window (typically 5 minutes). An attacker who captures a valid webhook cannot replay it after the window expires. Combine timestamp validation with idempotency keys for defense in depth against both replay and duplicate delivery.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →