Webhook Security: IP Whitelist and More
In this tutorial, you will learn about Webhook Security: IP Whitelist and More. We cover key concepts, practical examples, and best practices to help you master this topic.
Learn webhook security: IP whitelisting, payload encryption, HTTPS enforcement, secret rotation, rate limiting, input validation, and defense-in-depth strategies for webhook systems.
What You Learn
You will learn how to secure webhook systems with multiple defense layers: IP whitelisting to restrict sources, payload encryption for sensitive data, HTTPS enforcement, input validation to prevent injection attacks, and a defense-in-depth security strategy.
Why It Matters
Webhook endpoints are public-facing HTTP endpoints vulnerable to attacks: fake webhooks, replay attacks, SSRF, injection, and data interception. A single security breach can lead to data theft, unauthorized actions, and Compliance violations. Multiple security layers protect against these threats.
Real-World Use
DodaTech's webhook system uses 4 security layers: IP whitelisting restricts to known provider IP ranges, HMAC signature verification authenticates payloads, payload encryption protects sensitive threat data, and input validation prevents injection attacks. In 2025, these layers blocked 12000 unauthorized requests.
IP Whitelisting
// IP whitelist middleware
const ipRangeCheck = require('ip-range-check');
const PROVIDER_IP_RANGES = {
stripe: [
'13.0.0.0/8',
'52.0.0.0/8',
'54.0.0.0/8',
],
github: [
'192.30.252.0/22',
'185.199.108.0/22',
'140.82.112.0/20',
],
};
function ipWhitelistMiddleware(allowedRanges) {
return (req, res, next) => {
const clientIp = req.ip || req.connection.remoteAddress;
if (!ipRangeCheck(clientIp, allowedRanges)) {
console.error(`Blocked request from ${clientIp}`);
return res.status(403).json({
error: 'Access denied',
code: 'IP_NOT_WHITELISTED',
});
}
next();
};
}
// Usage per provider route
app.post('/webhooks/stripe',
ipWhitelistMiddleware(PROVIDER_IP_RANGES.stripe),
verifyStripeSignature,
handleWebhook
);
app.post('/webhooks/github',
ipWhitelistMiddleware(PROVIDER_IP_RANGES.github),
verifyGithubSignature,
handleWebhook
);
Expected output: Requests from IPs outside the whitelisted ranges are blocked with 403. Each provider has its own IP range list. IP whitelisting is the first line of defense before signature verification.
Payload Encryption
const crypto = require('crypto');
// Encrypt sensitive webhook payload data
class WebhookPayloadEncryption {
constructor(encryptionKey) {
this.algorithm = 'aes-256-gcm';
this.key = crypto.scryptSync(
encryptionKey,
'webhook-salt',
32
);
}
encryptSensitiveData(data, fields) {
const encrypted = { ...data };
for (const field of fields) {
if (encrypted[field]) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(
this.algorithm,
this.key,
iv
);
let encryptedValue = cipher.update(
JSON.stringify(encrypted[field]),
'utf8',
'hex'
);
encryptedValue += cipher.final('hex');
const authTag = cipher.getAuthTag().toString('hex');
encrypted[field] = {
encrypted: true,
iv: iv.toString('hex'),
authTag,
value: encryptedValue,
};
}
}
return encrypted;
}
decryptSensitiveData(data) {
if (!data.encrypted) return data;
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(data.iv, 'hex')
);
decipher.setAuthTag(Buffer.from(data.authTag, 'hex'));
let decrypted = decipher.update(data.value, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return JSON.parse(decrypted);
}
}
// Usage on provider side
const encryption = new WebhookPayloadEncryption(process.env.ENCRYPTION_KEY);
const payload = {
event: 'payment.succeeded',
data: encryption.encryptSensitiveData(
{ cardNumber: '4111XXXXXXXX1111', cvv: '123' },
['cardNumber', 'cvv']
),
};
Expected output: Sensitive fields are encrypted with AES-256-GCM before storage or transmission. Each field gets a unique IV and authentication tag. Only authorized consumers with the key can decrypt.
Input Validation
const { z } = require('zod');
// Zod schemas for webhook payload validation
const StripeWebhookSchema = z.object({
id: z.string(),
type: z.string(),
data: z.object({
object: z.object({
id: z.string(),
amount: z.number().positive(),
currency: z.string().length(3),
customer: z.string().optional(),
status: z.enum(['succeeded', 'pending', 'failed']),
}),
}),
created: z.number(),
});
const GitHubWebhookSchema = z.object({
ref: z.string(),
before: z.string(),
after: z.string(),
repository: z.object({
id: z.number(),
name: z.string(),
full_name: z.string(),
url: z.string().url(),
}),
sender: z.object({
login: z.string(),
id: z.number(),
}),
});
// Validation middleware
function validateWebhookPayload(schema) {
return (req, res, next) => {
const result = schema.safeParse(req.body);
if (!result.success) {
const errors = result.error.errors.map(e => ({
path: e.path.join('.'),
message: e.message,
}));
webhookLogger.warn('Payload validation failed', {
path: req.path,
errors,
payload: sanitizePayload(req.body),
});
return res.status(422).json({
error: 'Validation failed',
details: errors,
});
}
req.validatedBody = result.data;
next();
};
}
// SQL injection prevention
function sanitizeForDatabase(value) {
if (typeof value === 'string') {
// Remove SQL injection patterns
return value.replace(/['";\\]/g, '')
.replace(/--/g, '')
.replace(/\/\*/g, '');
}
return value;
}
Expected output: Zod schemas validate payload structure and types before processing. Malformed payloads return 422 with detailed error messages. String values are sanitized for database queries to prevent SQL Injection.
HTTPS Enforcement
// HTTPS middleware
function enforceHttps(req, res, next) {
if (req.headers['x-forwarded-proto'] === 'https') {
return next();
}
if (req.secure) {
return next();
}
// Check if the request came from a provider that uses HTTPS
const userAgent = req.headers['user-agent'] || '';
const knownProviders = ['stripe', 'github', 'svix', 'sendgrid'];
const isProvider = knownProviders.some(p =>
userAgent.toLowerCase().includes(p)
);
if (isProvider) {
console.error(`Non-HTTPS request from provider: ${userAgent}`);
return res.status(400).json({
error: 'HTTPS required',
message: 'Webhook endpoints require HTTPS',
});
}
next();
}
// Consumer-side: only register HTTPS URLs
function validateSubscriberUrl(url) {
try {
const parsed = new URL(url);
if (parsed.protocol !== 'https:') {
throw new Error('URL must use HTTPS');
}
if (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1') {
throw new Error('URL must not be localhost');
}
// Block private IP ranges
const privateRanges = [
/^10\./,
/^172\.(1[6-9]|2\d|3[01])\./,
/^192\.168\./,
/^169\.254\./,
];
if (privateRanges.some(range => range.test(parsed.hostname))) {
throw new Error('URL must not be on private network');
}
return true;
} catch (err) {
throw new Error(`Invalid subscriber URL: ${err.message}`);
}
}
Expected output: HTTPS is enforced for incoming webhooks and subscriber URLs. Only HTTPS URLs can be registered as webhook endpoints. Non-HTTPS requests from providers are rejected.
Security Headers
// Security headers middleware for webhook endpoints
function webhookSecurityHeaders(req, res, next) {
// Prevent content type sniffing
res.setHeader('X-Content-Type-Options', 'nosniff');
// Prevent clickjacking
res.setHeader('X-Frame-Options', 'DENY');
// Enable XSS filter
res.setHeader('X-XSS-Protection', '1; mode=block');
// Strict transport security
res.setHeader(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains'
);
// No caching for webhook responses
res.setHeader('Cache-Control', 'no-store');
// Content Security Policy
res.setHeader(
'Content-Security-Policy',
"default-src 'none'"
);
next();
}
app.use('/webhooks', webhookSecurityHeaders);
Expected output: Security headers protect against common web attacks. HSTS enforces HTTPS. Cache-Control prevents Caching of webhook requests. CSP restricts resource loading.
Common Mistakes
1. IP Whitelisting as Sole Security
IP addresses can be spoofed or changed. Providers occasionally change IP ranges. Use IP whitelisting as the first layer, not the only layer. Always verify signatures regardless of IP.
2. No Input Validation
Webhook payloads can contain malicious data: SQL injection in string fields, script injection in URLs, oversized payloads for DoS. Validate all fields. Sanitize before database operations.
3. Storing Sensitive Data in Plaintext
Storing credit card numbers, API keys, or PII from webhook payloads in plaintext violates compliance requirements. Encrypt sensitive fields at rest. Never log sensitive data.
4. HTTP Instead of HTTPS
Webhook payloads sent over HTTP are visible to network intermediaries. All webhook endpoints and subscriber URLs must use HTTPS. Reject non-HTTPS requests and URLs.
5. No Rate Limiting on Security Endpoints
Attackers can brute-force signature verification or probe endpoints. Implement rate limiting on webhook endpoints. Block IPs with excessive failed verification attempts.
Practice Questions
1. Why is IP whitelisting not sufficient for webhook security?
IP addresses can be spoofed. Providers change IP ranges without notice. Multi-tenant systems share IPs. IP whitelisting is useful as a defense-in-depth layer but not as the sole security measure.
2. How do you encrypt sensitive data in webhook payloads?
Use AES-256-GCM. Generate a random IV for each encryption. Include IV and auth tag with the encrypted data. The consumer uses the shared key to decrypt. Never reuse IVs.
3. What input validation should webhook payloads undergo?
Type validation (strings are strings, numbers are numbers), range validation (amounts are positive), format validation (emails, URLs), length limits, and SQL/NoSQL injection pattern detection.
4. Why must webhook URLs use HTTPS?
HTTPS encrypts the payload in transit, preventing interception and tampering. Without HTTPS, any network intermediary can read or modify webhook data. HTTPS also authenticates the server identity.
Challenge
Build a secure webhook system with: IP whitelist middleware for each provider, Zod payload validation for all event types, AES-256-GCM encryption for sensitive fields, HTTPS enforcement (reject non-HTTPS and private IPs), security headers middleware, rate limiting on security failures, and audit logging of all security events.
FAQ
Mini Project: Webhook Security Audit
Build a security audit tool that: tests webhook endpoints for HTTPS enforcement, verifies signature verification is implemented, checks IP whitelisting, tests input validation with malicious payloads, validates sensitive data encryption, checks security headers, and generates a security report card.
What's Next
Now that you understand webhook security, learn about Secret Rotation to manage webhook signing secrets securely.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro