Learn how to verify webhook signatures using HMAC-SHA256 to ensure payloads are authentic.
Webhook Security
Verify webhook signatures to ensure payloads are authentic.
Why Verify Signatures?
Anyone who knows your webhook URL could send fake events. Signature verification proves the payload came from cStar.
How Signing Works
- cStar creates a signature using your webhook's secret
- The signature is sent in the
X-Signatureheader - You compute the expected signature and compare
The X-Signature Header
X-Signature: sha256=d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592
The format is sha256= followed by the hex-encoded HMAC-SHA256 signature.
Verification Code
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-signature'];
const isValid = verifyWebhookSignature(
req.body,
signature,
process.env.WEBHOOK_SECRET
);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
console.log('Received event:', req.body.type);
res.sendStatus(200);
});
Python
import hmac
import hashlib
import json
def verify_webhook_signature(payload, signature, secret):
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
json.dumps(payload, separators=(',', ':')).encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
payload = request.get_json()
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return 'Invalid signature', 401
# Process the webhook
print(f"Received event: {payload['type']}")
return 'OK', 200
PHP
function verifyWebhookSignature($payload, $signature, $secret) {
$expected = 'sha256=' . hash_hmac(
'sha256',
json_encode($payload, JSON_UNESCAPED_SLASHES),
$secret
);
return hash_equals($expected, $signature);
}
// Usage
$payload = json_decode(file_get_contents('php://input'), true);
$signature = $_SERVER['HTTP_X_SIGNATURE'];
if (!verifyWebhookSignature($payload, $signature, $webhookSecret)) {
http_response_code(401);
exit('Invalid signature');
}
Security Best Practices
Do:
- Always verify signatures before processing
- Use timing-safe comparison functions
- Store secrets in environment variables
- Rotate secrets periodically
- Log failed verification attempts
Don't:
- Skip verification in production
- Use simple string comparison (timing attack risk)
- Hardcode secrets in code
- Log the full payload (may contain PII)
Rotating Secrets
If you suspect a secret is compromised:
- Go to Settings → Integrations → Webhooks
- Click the webhook to edit
- Click Regenerate Secret
- Update your server with the new secret
- Old deliveries will still use the old secret
Replay Protection
Use the X-Event-ID header to prevent replay attacks:
const processedEvents = new Set();
function handleWebhook(req) {
const eventId = req.headers['x-event-id'];
if (processedEvents.has(eventId)) {
return; // Already processed
}
processedEvents.add(eventId);
// Process event...
}