Webhook Security Best Practices
Securing your webhook endpoints is critical to prevent unauthorized access and ensure data integrity.
Security Checklist
Use this checklist to ensure your webhook implementation is secure:
| # | Practice | Priority | Status |
|---|---|---|---|
| 1 | Use HTTPS endpoints only | 🔴 Critical | ☐ |
| 2 | Verify webhook secret token | 🔴 Critical | ☐ |
| 3 | Validate payload structure | 🟡 High | ☐ |
| 4 | Implement rate limiting | 🟡 High | ☐ |
| 5 | Handle duplicates (idempotency) | 🟡 High | ☐ |
| 6 | Log all requests | 🟢 Medium | ☐ |
| 7 | Set up monitoring/alerts | 🟢 Medium | ☐ |
Authentication Methods
Secret Token Verification
Required
Always verify webhook authenticity using a secret token header.
Step 1: Configure secret header in Pollarix
| Header Key | Value |
|---|---|
X-Webhook-Secret | your-random-secret-here |
Step 2: Generate a secure secret
Generate secure token
openssl rand -hex 32
# Output: a1b2c3d4e5f6789...
Step 3: Verify in your handler
- Node.js
- Python
Secure webhook handler
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
app.post('/webhooks/pollarix', (req, res) => {
const receivedSecret = req.headers['x-webhook-secret'];
if (receivedSecret !== WEBHOOK_SECRET) {
console.warn('⚠️ Invalid webhook secret!', {
ip: req.ip,
timestamp: new Date().toISOString()
});
return res.status(401).send('Unauthorized');
}
// Secret verified - process webhook
processWebhook(req.body);
res.status(200).send('OK');
});
Secure webhook handler
import os
from flask import Flask, request, abort
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')
@app.route('/webhooks/pollarix', methods=['POST'])
def handle_webhook():
received_secret = request.headers.get('X-Webhook-Secret')
if received_secret != WEBHOOK_SECRET:
print(f'⚠️ Invalid webhook secret from {request.remote_addr}')
abort(401)
# Secret verified - process webhook
process_webhook(request.json)
return 'OK', 200
Bearer Token Authentication
Use standard Bearer token authentication:
| Header | Value |
|---|---|
Authorization | Bearer your-api-token |
Bearer token verification
app.post('/webhooks/pollarix', (req, res) => {
const authHeader = req.headers['authorization'];
const expectedToken = `Bearer ${process.env.WEBHOOK_TOKEN}`;
if (authHeader !== expectedToken) {
return res.status(401).send('Unauthorized');
}
// Process webhook...
res.status(200).send('OK');
});
HTTPS Requirements
Never Use HTTP
Plain HTTP exposes your webhook data to:
- Man-in-the-middle attacks - Data can be intercepted
- Spoofing - Attackers can impersonate Pollarix
- Data leakage - Survey responses exposed
Free SSL Options
| Provider | Type | Effort |
|---|---|---|
| Let's Encrypt | Free automated | Low |
| Cloudflare | Free proxy SSL | Low |
| AWS ACM | Free (with AWS) | Medium |
Payload Validation
Always validate incoming payloads before processing:
Payload validation with Joi
const Joi = require('joi');
const webhookSchema = Joi.object({
event: Joi.string().valid(
'SURVEY_COMPLETED',
'SURVEY_STATUS_CHANGED',
'COUPON_REDEEMED'
).required(),
timestamp: Joi.string().isoDate().required(),
webhookId: Joi.string().required(),
departmentId: Joi.string().required(),
data: Joi.object().required()
});
app.post('/webhooks/pollarix', (req, res) => {
const { error, value } = webhookSchema.validate(req.body);
if (error) {
console.error('Invalid payload:', error.message);
return res.status(400).send('Invalid payload');
}
processWebhook(value);
res.status(200).send('OK');
});
Rate Limiting
Protect your endpoint from abuse:
Rate limiting with express-rate-limit
const rateLimit = require('express-rate-limit');
const webhookLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: 'Too many webhook requests',
handler: (req, res) => {
console.warn('Rate limit exceeded:', req.ip);
res.status(429).send('Too many requests');
}
});
app.post('/webhooks/pollarix', webhookLimiter, (req, res) => {
// Handle webhook
});
Idempotency
Handle duplicate webhook deliveries safely:
Idempotent webhook handler
const processedEvents = new Map();
const EVENT_TTL = 24 * 60 * 60 * 1000; // 24 hours
function cleanupOldEvents() {
const now = Date.now();
for (const [key, timestamp] of processedEvents) {
if (now - timestamp > EVENT_TTL) {
processedEvents.delete(key);
}
}
}
// Run cleanup hourly
setInterval(cleanupOldEvents, 60 * 60 * 1000);
app.post('/webhooks/pollarix', (req, res) => {
const { webhookId, timestamp } = req.body;
const eventKey = `${webhookId}-${timestamp}`;
if (processedEvents.has(eventKey)) {
return res.status(200).send('Already processed');
}
processedEvents.set(eventKey, Date.now());
// Process webhook...
res.status(200).send('OK');
});
Common Security Mistakes
- ❌ Insecure
- ✅ Secure
DON'T do this
// No secret verification
app.post('/webhooks', (req, res) => {
// Trusting any request 😱
processWebhook(req.body);
res.send('OK');
});
DO this instead
app.post('/webhooks', (req, res) => {
// Verify secret
if (req.headers['x-webhook-secret'] !== SECRET) {
return res.status(401).send('Unauthorized');
}
// Validate payload
if (!isValidPayload(req.body)) {
return res.status(400).send('Invalid');
}
// Process async
processWebhookAsync(req.body);
res.send('OK');
});
Logging and Monitoring
Log All Webhook Requests
Structured logging
const winston = require('winston');
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'webhooks.log' })
]
});
app.post('/webhooks/pollarix', (req, res) => {
logger.info('Webhook received', {
event: req.body.event,
webhookId: req.body.webhookId,
timestamp: req.body.timestamp,
ip: req.ip,
userAgent: req.headers['user-agent']
});
// Process webhook...
});
Monitor for Anomalies
Set up alerts for:
- Unusual spike in webhook volume
- High error rates
- Requests from unexpected IPs
- Invalid authentication attempts
Error Handling
Don't Expose Internal Errors
Safe error handling
app.post('/webhooks/pollarix', async (req, res) => {
try {
await processWebhook(req.body);
res.status(200).send('OK');
} catch (error) {
// Log detailed error internally
console.error('Webhook processing failed:', error);
// Return generic error to client
res.status(500).send('Internal error');
}
});
Graceful Degradation
Queue for retry on failure
app.post('/webhooks/pollarix', async (req, res) => {
// Always acknowledge receipt quickly
res.status(200).send('OK');
try {
await processWebhook(req.body);
} catch (error) {
// Queue for retry
await webhookQueue.add('retry', {
payload: req.body,
attempts: 0
});
}
});
Summary
| Practice | Priority | Difficulty |
|---|---|---|
| Use HTTPS | 🔴 Critical | Low |
| Token authentication | 🔴 Critical | Low |
| Payload validation | 🟡 High | Medium |
| Idempotency handling | 🟡 High | Medium |
| Rate limiting | 🟢 Medium | Low |
| Logging & monitoring | 🟢 Medium | Low |
Next Steps
- Configuration Guide - Set up your webhooks
- Events Reference - Understand event payloads
- API Reference - Manage webhooks programmatically
Was this page helpful?