How to Secure Webhook Endpoints in a SaaS App
OverMCP Team
TL;DR: How to Secure Webhook Endpoints in a SaaS App
To secure webhook endpoints in a SaaS app, you must validate incoming requests by verifying a HMAC signature using a shared secret, whitelist IP ranges from the webhook provider, and enforce rate limiting. These steps prevent forged requests, replay attacks, and abuse. Below is a complete guide with code examples.
Why Webhook Endpoints Are Vulnerable
Webhooks are HTTP callbacks that deliver real-time data from services like Stripe, GitHub, or Slack. Because they are public URLs, they can be targeted by attackers who send fake payloads to trigger unauthorized actions (e.g., refunding orders, deleting repos). Common risks include:
Step 1: Verify Webhook Signatures
Most providers send a signature header (e.g., X-Hub-Signature-256 or Stripe-Signature). Compute the HMAC of the raw body using your shared secret and compare it.
Example: Express.js with Stripe
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook/stripe', express.raw({type: 'application/json'}), (req, res) => {
const sig = req.headers['stripe-signature'];
const secret = 'whsec_your_secret_here';
const payload = req.body.toString(); // raw buffer
const expectedSig = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
// Stripe actually uses a more complex scheme with timestamps
// For simplicity, use stripe's official library:
// const event = stripe.webhooks.constructEvent(payload, sig, secret);
if (sig !== `t=...,v1=${expectedSig}`) {
return res.status(400).send('Signature mismatch');
}
// Process event
res.json({ received: true });
});Always validate signatures before processing. Use the provider's official library if available.
Step 2: Whitelist IP Addresses
Providers publish their IP ranges. Restrict your endpoint to accept traffic only from those IPs.
const ALLOWED_IPS = ['192.168.1.0/24', '10.0.0.0/8'];
app.use('/webhook', (req, res, next) => {
const ip = req.ip || req.connection.remoteAddress;
if (!ALLOWED_IPS.some(range => ipInCIDR(ip, range))) {
return res.status(403).send('Forbidden');
}
next();
});Note: IPs can change, so update your list periodically. For production, use a firewall or API gateway.
Step 3: Enforce Rate Limiting
Prevent abuse by limiting requests per IP or per time window.
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 1 * 60 * 1000, // 1 minute
max: 10, // 10 requests per minute
message: 'Too many requests, try later.'
});
app.use('/webhook', limiter);Adjust limits based on expected webhook volume.
Step 4: Use HTTPS Only
Ensure your webhook endpoint is served over HTTPS. This encrypts the payload and signature, preventing man-in-the-middle attacks. Enforce this at the server level (e.g., redirect HTTP to HTTPS) or use a reverse proxy.
Step 5: Idempotency Keys
Webhooks can be retried, leading to duplicate processing. Use an idempotency key (often in the payload) to safely handle duplicates.
const processed = new Set();
app.post('/webhook', (req, res) => {
const id = req.body.id; // or header
if (processed.has(id)) {
return res.status(200).send('Already processed');
}
processed.add(id);
// Process event
});For distributed systems, store keys in a database with a TTL.
Step 6: Validate Payload Schema
Check that the incoming JSON matches the expected structure. This prevents injection attacks and logical errors.
const Joi = require('joi');
const schema = Joi.object({
event: Joi.string().valid('payment_intent.succeeded'),
data: Joi.object({
id: Joi.string().required()
})
});
app.post('/webhook', (req, res) => {
const { error } = schema.validate(req.body);
if (error) return res.status(400).send('Invalid payload');
// ...
});Step 7: Log and Monitor
Log all webhook requests (including failures) for auditing. Monitor for anomalies like sudden spikes or invalid signatures.
app.post('/webhook', (req, res) => {
logger.info({ ip: req.ip, headers: req.headers, body: req.body }, 'Webhook received');
// ...
});Automating Security Scanning with OverMCP
Manually checking all these steps across services is tedious. Tools like OverMCP can scan your SaaS app's endpoints for missing signature verification, exposed secrets, and other webhook vulnerabilities. OverMCP integrates into your CI/CD pipeline to catch issues before deployment.
Conclusion
Securing webhook endpoints is not optional. By implementing signature verification, IP whitelisting, rate limiting, HTTPS, idempotency, schema validation, and logging, you protect your SaaS from common attacks. Start with HMAC verification – it's the most critical step.
FAQ
What is the most important step to secure webhook endpoints?
Verify the HMAC signature using a shared secret. Without it, anyone can send fake payloads and trigger actions.
Can I rely solely on IP whitelisting?
No, IPs can change or be spoofed. Always combine IP whitelisting with signature verification for defense in depth.
How do I handle webhook retries without duplicates?
Use idempotency keys. Store the unique event ID from the payload and reject subsequent requests with the same ID.