Webhook Security Guide
Best practices for securing webhook deliveries โ signature verification, IP whitelisting, and more.
HMAC-SHA256 Signature Verification
Every webhook is signed using Standard Webhooks HMAC-SHA256. The signature is included in the webhook-signature header.
Format: v1,{base64(hmac_signature)}
Node.js Verification
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signatureHeader: string,
secret: string
): boolean {
// Parse "v1,{base64}" format
const parts = signatureHeader.split(',');
const version = parts[0];
const signature = parts[1];
if (version !== 'v1') return false;
// Compute expected signature
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['webhook-signature'] as string;
const secret = 'whsec_your_signing_secret';
if (!verifyWebhookSignature(req.body.toString(), signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
const event = JSON.parse(req.body);
console.log('Verified event:', event.event);
res.status(200).json({ received: true });
});Python Verification
import hmac
import hashlib
import base64
import type { Metadata } from 'next';
// Revalidate every hour for ISR
export const revalidate = 3600;
export const metadata: Metadata = {
title: 'Security',
description: 'How HookSniff secures your webhook deliveries with signatures and encryption',
};
def verify_webhook_signature(
payload: bytes,
signature_header: str,
secret: str
) -> bool:
"""Verify Standard Webhooks HMAC-SHA256 signature."""
parts = signature_header.split(',')
if len(parts) != 2 or parts[0] != 'v1':
return False
signature = parts[1]
# Compute expected signature
expected = base64.b64encode(
hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).digest()
).decode('utf-8')
return hmac.compare_digest(signature, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('webhook-signature', '')
secret = 'whsec_your_signing_secret'
if not verify_webhook_signature(request.data, signature, secret):
abort(401)
event = request.json
return {'received': True}, 200Timestamp Validation
Always validate the webhook-timestamp header to prevent replay attacks. Reject webhooks older than 5 minutes:
function isTimestampValid(timestampHeader: string, toleranceSec = 300): boolean {
const webhookTime = parseInt(timestampHeader, 10);
const now = Math.floor(Date.now() / 1000);
return Math.abs(now - webhookTime) <= toleranceSec;
}
// In your handler:
const timestamp = req.headers['webhook-timestamp'] as string;
if (!isTimestampValid(timestamp)) {
return res.status(401).json({ error: 'Timestamp expired' });
}IP Whitelisting
For additional security, restrict incoming webhooks to HookSniff's IP addresses:
# Fetch current outbound IPs
curl https://hooksniff-api-1046140057667.europe-west1.run.app/v1/outbound-ipsUse these IPs in your firewall or reverse proxy configuration. Note: IPs may change โ fetch periodically or subscribe to changes.
TLS Enforcement
HookSniff only delivers webhooks to https:// endpoints. HTTP endpoints are rejected unless explicitly allowed. All API communication is encrypted via TLS.
SSRF Protection
HookSniff blocks webhook delivery to internal/private networks to prevent SSRF attacks:
localhost,127.0.0.1,::1- Private IP ranges:
10.*,172.16-31.*,192.168.* - Link-local:
169.254.* - Internal domains:
*.local,*.internal,*.localhost - Hex-encoded IPs:
0x7f000001