| title | description |
|---|---|
Webhook Security |
Learn how to verify webhook signatures and implement secure webhook handling |
Webhook requests include an HMAC-SHA256 signature in the X-Webhook-Signature-256 header that you should verify to ensure authenticity and prevent tampering.
- Extract the signature from the
X-Webhook-Signature-256header (format:sha256=<hex-signature>) - Get the raw request body before parsing
- Calculate the expected signature using your webhook signing secret
- Compare signatures using a timing-safe comparison function
- Process the webhook only if signatures match
import crypto from 'node:crypto';
import express from 'express';
function verifyWebhookSignature(
signingSecret: string,
payload: string,
signature: string
): boolean {
if (!signingSecret || !payload || !signature?.startsWith('sha256=')) {
return false;
}
try {
const receivedSignature = signature.substring(7);
const expectedSignature = crypto
.createHmac('sha256', signingSecret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
} catch (error) {
console.error('Signature verification failed:', error);
return false;
}
}- Store signing secrets in environment variables, never hardcode them
- Always verify signatures before processing webhook data
- Use timing-safe comparison functions (
crypto.timingSafeEqual()in Node.js,hmac.compare_digest()in Python) - Only accept webhooks over HTTPS
- Log verification failures for security monitoring
- Implement rate limiting on webhook endpoints
Solution: Use the raw request body string before any parsing:
- Express:
express.raw({ type: 'application/json' })andreq.body.toString() - Flask:
request.get_data(as_text=True) - FastAPI:
await request.body()then.decode('utf-8')
Solution: Verify the X-Webhook-Signature-256 header is present and starts with sha256=. Check for header case sensitivity in your framework.
Solution: Ensure consistent UTF-8 encoding throughout your verification process.