Why Webhook Security Matters
Webhook endpoints are publicly accessible URLs that accept HTTP POST requests. Without proper security, attackers can:
This guide covers production-ready security practices to protect your webhook endpoints.
1. Always Verify Webhook Signatures
Signature verification is the most critical security practice. It ensures webhooks actually come from the expected source and haven't been tampered with.
How Signature Verification Works
The webhook provider computes an HMAC hash of the request body using a shared secret key
The signature is sent in a request header (e.g., X-Webhook-Signature)
Your endpoint computes the same HMAC hash using the same secret
If the signatures match, the webhook is authentic
Example: Verifying Stripe Webhooks (Node.js)
const crypto = require('crypto');
function verifyStripeSignature(payload, signature, secret) {
// Extract timestamp and signature from header
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t=')).split('=')[1];
const sig = elements.find(e => e.startsWith('v1=')).split('=')[1];
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Compare signatures using timing-safe comparison
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expectedSig))) {
throw new Error('Invalid signature');
}
// Check timestamp to prevent replay attacks (5 min tolerance)
const currentTime = Math.floor(Date.now() / 1000);
if (currentTime - parseInt(timestamp) > 300) {
throw new Error('Webhook timestamp too old');
}
return true;
}
// Usage in Express.js
app.post('/webhooks/stripe', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['stripe-signature'];
const payload = req.body.toString();
try {
verifyStripeSignature(payload, signature, process.env.STRIPE_WEBHOOK_SECRET);
// Process webhook...
res.sendStatus(200);
} catch (err) {
console.error('Webhook signature verification failed:', err.message);
res.sendStatus(400);
}
});Key Takeaways
- • Use
crypto.timingSafeEqual()to prevent timing attacks - • Always validate timestamps to prevent replay attacks
- • Use the raw request body (before parsing) for signature computation
- • Store webhook secrets in environment variables, never in code
2. Always Use HTTPS
HTTPS encrypts webhook data in transit, preventing man-in-the-middle attacks and eavesdropping.
❌ Never Use HTTP
- • Payloads can be intercepted
- • Signatures can be stolen
- • Data is sent in plaintext
- • Vulnerable to MITM attacks
✓ Always Use HTTPS
- • End-to-end encryption
- • Certificate validation
- • Industry standard
- • Free with Let's Encrypt
Most webhook providers require HTTPS and will refuse to send webhooks to HTTP endpoints.
3. Implement Rate Limiting
Rate limiting protects your endpoint from abuse and DDoS attacks. Limit requests per IP address, per source, or globally.
Example: Rate Limiting with Express.js
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many webhook requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
app.post('/webhooks/github', webhookLimiter, (req, res) => {
// Process webhook...
});Per-IP Rate Limiting
Limit requests from individual IP addresses to prevent single-source attacks
Per-Source Rate Limiting
Limit webhooks from specific providers to prevent misconfigured integrations from overwhelming your system
Global Rate Limiting
Set an overall limit on webhook processing to protect backend resources
4. Validate Payload Structure
Never trust webhook payloads. Always validate structure and sanitize data before processing.
Example: Schema Validation with Zod
import { z } from 'zod';
const PaymentWebhookSchema = z.object({
id: z.string(),
type: z.enum(['payment.succeeded', 'payment.failed']),
data: z.object({
amount: z.number().positive(),
currency: z.string().length(3),
customer_id: z.string(),
}),
created: z.number(),
});
app.post('/webhooks/payment', async (req, res) => {
try {
// Validate payload structure
const webhook = PaymentWebhookSchema.parse(req.body);
// Process validated webhook
await processPayment(webhook);
res.sendStatus(200);
} catch (error) {
console.error('Invalid webhook payload:', error);
res.sendStatus(400);
}
});Validation Checklist
- Validate required fields are present
- Check data types match expectations
- Sanitize string inputs to prevent injection attacks
- Validate enum values are within expected set
- Reject unexpected or malformed payloads
5. Implement Idempotency
Webhooks provide at-least-once delivery, meaning you may receive the same webhook multiple times. Your handlers must be idempotent.
Example: Idempotency with Database Tracking
async function processWebhook(webhook) {
const webhookId = webhook.id;
// Check if we've already processed this webhook
const existing = await db.processedWebhooks.findOne({ webhookId });
if (existing) {
console.log('Webhook already processed, skipping:', webhookId);
return; // Idempotent: safe to receive multiple times
}
// Process the webhook
await handlePayment(webhook.data);
// Mark as processed
await db.processedWebhooks.insert({
webhookId,
processedAt: new Date(),
});
}Alternative approaches:
- •Use webhook IDs as database unique constraints
- •Implement distributed locks for critical operations
- •Design operations to be naturally idempotent (e.g., SET instead of INCREMENT)
Webhook Security Checklist
Verify webhook signatures on every request
Use HMAC validation with timing-safe comparison
Use HTTPS for all webhook endpoints
Never accept webhooks over HTTP
Implement rate limiting per IP and per source
Protect against DDoS and misconfigured integrations
Validate payload structure and sanitize inputs
Use schema validation libraries like Zod or Joi
Make webhook handlers idempotent
Track processed webhooks to handle duplicates safely
Check timestamps to prevent replay attacks
Reject webhooks older than 5-10 minutes
Store secrets securely in environment variables
Never commit secrets to version control
Log webhook attempts for security monitoring
Track failed verifications and suspicious patterns
Return 200 OK quickly, process asynchronously
Prevent timeouts and retry storms
Monitor webhook endpoint health and errors
Set up alerts for verification failures
Secure Your Webhooks with hookVM
hookVM handles signature verification, rate limiting, and security best practices out of the box. Focus on your application logic, not security infrastructure.