Documentation

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êteFormat de la valeur
X-Savebase-Signaturesha256=abcdef0123...
X-Savebase-Eventpost.saved
User-AgentSavebase-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 '', 200

PHP

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 OK sous 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 → Webhooks si 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), ou hash_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.