Sécurité et signature HMAC
Chaque requête webhook envoyée par Savebase est signée avec HMAC-SHA256 pour confirmer qu’elle provient réellement de Savebase et n’a pas été altérée en transit. Vérifiez toujours cette signature avant de traiter un événement webhook.
Fonctionnement de la signature
Savebase calcule la signature en appliquant HMAC-SHA256 au corps brut de la requête avec la valeur suivante de votre webhook : Secret de signature comme clé. Le résultat est envoyé dans X-Savebase-Signature en-tête sous la forme sha256=<hex-digest>.
Utiliser le corps brut
Calculez toujours le HMAC sur le octets bruts et non modifiés du corps de la requête — pas un objet JSON re-sérialisé. Analyser puis re-sérialiser le JSON peut modifier les espaces et l’ordre des clés et faire échouer la vérification de signature.
| En-tête | Format de la valeur |
|---|---|
X-Savebase-Signature | sha256=abcdef0123... |
X-Savebase-Event | post.saved |
User-Agent | Savebase-Webhook/1.0 |
Node.js / Express
bash
const crypto = require('crypto');
function verifySignature(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected, 'utf8'),
Buffer.from(signatureHeader, 'utf8')
);
}
app.post(
'/savebase-webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-savebase-signature'];
if (!sig || !verifySignature(req.body, sig, process.env.SAVEBASE_WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Event:', event.event, event.data.id);
res.sendStatus(200);
}
);Python / Flask
bash
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['SAVEBASE_WEBHOOK_SECRET']
@app.route('/savebase-webhook', methods=['POST'])
def handle_webhook():
sig_header = request.headers.get('X-Savebase-Signature', '')
raw_body = request.get_data()
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, sig_header):
abort(401)
payload = request.get_json()
print('Received event:', payload['event'])
return '', 200PHP
bash
<?php
$secret = getenv('SAVEBASE_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$sigHeader = $_SERVER['HTTP_X_SAVEBASE_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (!hash_equals($expected, $sigHeader)) {
http_response_code(401);
exit('Invalid signature');
}
$event = json_decode($rawBody, true);
error_log('Received: ' . $event['event']);
http_response_code(200);Bonnes pratiques de sécurité
- Répondez rapidement. Renvoyer
200 OKsous 10 secondes. Savebase interrompt après 10 s et marque l’envoi comme échoué. Déportez les tâches lourdes vers un traitement en arrière-plan. - N’inscrivez jamais votre secret de signature dans les journaux. Traitez-le comme un mot de passe. Renouvelez-le dans
Settings → Webhookssi vous pensez qu’il a été compromis. - Utilisez uniquement HTTPS. Savebase refuse les adresses de destination HTTP non chiffrées.
- Utilisez une comparaison résistante aux attaques temporelles. Utilisez toujours
crypto.timingSafeEqual(Node),hmac.compare_digest(Python), ouhash_equals(PHP) — jamais une simple comparaison===. - Gérez vous-même les échecs. Savebase enregistre le résultat de l’envoi, mais l’implémentation actuelle ne réessaie pas et ne rejoue pas automatiquement les requêtes échouées.