Skip to content

Webhook Security: IP Whitelist and More

DodaTech Updated 2026-06-28 7 min read

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

What are the official IP ranges for Stripe webhooks?

Stripe publishes IP ranges at https://stripe.com/docs/ips. They change periodically. Automate IP whitelist updates by fetching from the provider's published list periodically.

Should I encrypt all webhook data or just sensitive fields?

Encrypt only sensitive fields (PII, payment data, credentials). Full payload encryption adds latency and complexity. Use field-level encryption for targeted protection.

How do I handle providers that change IP ranges?

Subscribe to provider IP change notifications. Update whitelist automatically. During transition, accept both old and new ranges. Remove old ranges after confirmation.

Can I use mutual TLS (mTLS) for webhook security?

Yes. mTLS provides bidirectional authentication. The provider presents a client certificate. The consumer verifies it. mTLS is more secure than IP whitelisting but requires provider support.

What is the OWASP recommendation for webhook security?

Use defense-in-depth: HTTPS, signature verification, IP whitelisting, input validation, rate limiting, and logging. Follow OWASP ASVS for API security requirements.

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