patternjavascriptMajor
Webhook Signature Verification
Viewed 0 times
webhookhmacsha256signatureverificationreplay attackstripe webhook
Error Messages
Problem
Webhook endpoints that accept payloads without signature verification can be spoofed—any attacker knowing the endpoint URL can send fake events to trigger business logic.
Solution
Compute an HMAC-SHA256 signature over the raw request body using the shared webhook secret. Compare it with the signature in the request header using a constant-time comparison.
Why
HMAC signatures prove the payload came from the party holding the shared secret. The raw body must be used—parsing JSON first changes whitespace and key ordering, invalidating the signature.
Gotchas
- Use the raw body buffer for signature computation—body parsers modify the payload
- Add a timestamp to the payload and reject webhooks older than 5 minutes to prevent replay attacks
- Stripe and GitHub use slightly different signature formats—read the provider's specific documentation
- Store the webhook secret in an environment variable, not in code
Code Snippets
Webhook signature verification with HMAC-SHA256
const crypto = require('crypto');
app.post('/webhooks/payment', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const timestamp = req.headers['x-webhook-timestamp'];
// Reject stale webhooks (replay protection)
const age = Date.now() - Number(timestamp);
if (age > 5 * 60 * 1000) {
return res.status(400).json({ error: 'Webhook too old' });
}
// Compute expected signature over timestamp + raw body
const payload = `${timestamp}.${req.body.toString()}`;
const expected = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(payload)
.digest('hex');
const sigBuffer = Buffer.from(signature, 'hex');
const expBuffer = Buffer.from(expected, 'hex');
if (sigBuffer.length !== expBuffer.length || !crypto.timingSafeEqual(sigBuffer, expBuffer)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Safe to process
const event = JSON.parse(req.body);
processWebhookEvent(event);
res.sendStatus(200);
});Revisions (0)
No revisions yet.