Security
Signature Verification
The implementation adds signature and timestamp headers to webhook delivery attempts. Recipients must verify the exact raw signed bytes, compare signatures safely, enforce a timestamp window, and deduplicate delivery IDs. Public delivery/replay abuse-case evidence is still being expanded.
Headers:
X-Webhook-Signature: HMAC-SHA256 signature of the payloadX-Webhook-Timestamp: Unix timestamp in seconds since epoch (UTC)
Signature Format:
sha256={hmac_hex_digest}
Verification Example (Node.js):
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Create HMAC using webhook secret
const hmac = crypto.createHmac('sha256', secret);
// Update with stringified payload
hmac.update(JSON.stringify(payload));
// Generate expected signature
const expectedSignature = `sha256=${hmac.digest('hex')}`;
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js middleware example
app.post('/webhooks/sso', (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
const payload = req.body;
// Limit the replay window; also persist and deduplicate delivery IDs
const currentTime = Math.floor(Date.now() / 1000);
if (Math.abs(currentTime - timestamp) > 300) {
return res.status(400).json({ error: 'Timestamp too old' });
}
// Verify signature
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook payload
console.log('Received event:', payload.event);
res.status(200).json({ status: 'received' });
});
Verification Example (Python):
import hmac
import hashlib
import time
import json
def verify_webhook_signature(payload, signature, secret):
# Create HMAC using webhook secret
hmac_gen = hmac.new(
secret.encode('utf-8'),
json.dumps(payload).encode('utf-8'),
hashlib.sha256
)
# Generate expected signature
expected_signature = f"sha256={hmac_gen.hexdigest()}"
# Use constant-time comparison
return hmac.compare_digest(signature, expected_signature)
# Flask example
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/webhooks/sso', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
timestamp = int(request.headers.get('X-Webhook-Timestamp'))
payload = request.json
# Verify timestamp
current_time = int(time.time())
if abs(current_time - timestamp) > 300:
return jsonify({'error': 'Timestamp too old'}), 400
# Verify signature
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
# Process webhook
print(f"Received event: {payload['event']}")
return jsonify({'status': 'received'}), 200
Security Best Practices:
- Always verify signatures before processing webhook payloads
- Validate timestamps to limit the replay window and deduplicate delivery IDs
- Validate signature lengths and use constant-time comparison to reduce timing leakage
- Store webhook secrets securely (environment variables, secret managers)
- Use HTTPS endpoints only
- Implement rate limiting on your webhook endpoints