HMAC Comment les webhooks savent que vous ne mentez pas
Chaque fois que GitHub, Stripe ou Shopify envoie un webhook vers votre serveur, ils signent le payload avec HMAC. Voici exactement comment cela fonctionne — et comment le vérifier dans votre propre code.
Vous recevez une requête POST affirmant provenir de Stripe. Le corps de la requête indique qu'un paiement a réussi. Vous traitez l'ordre, vous envoyez les produits — et trois jours plus tard, vous découvrez que la requête a été fausse. Ouch.
C'est pourquoi chaque fournisseur sérieux de webhooks signe ses corps de requête à l'aide de HMAC. Si vous comprenez l'HMAC, vous comprenez pourquoi GitHub, Stripe, Shopify, Twilio et presque tous les API modernes l'utilisent — et vous saurez exactement comment vérifier ces signatures dans votre propre code serveur.
Ce que l'HMAC est réellement
HMAC signifie Code d'authentification basé sur le hachage. Il répond à une seule question : « L'individu qui m'a envoyé ce message connaissait-elle le secret partagé ? »
Il fonctionne en exécutant une fonction de hachage cryptographique — généralement SHA-256 — sur la combinaison d'une clé secrète et du corps de la requête. Le résultat est une chaîne de caractères de longueur fixe qui :
- Change complètement si un seul octet du message change
- Ne peut pas être produit sans connaître la clé secrète
- Ne peut pas être inversé pour révéler la clé ou le message original
La formule est compacte : HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message)). Vous n'avez pas besoin de mémoriser les internals — chaque langage dispose d'une implémentation dans sa bibliothèque standard — mais il est utile de comprendre l'intention : la clé est mélangée dans le hachage deux fois, de manière différente, afin d'éviter une classe d'attaques appelée attaques de prolongation de longueur.
Comment un fournisseur de webhooks l'utilise
Lorsque vous inscrivez un point d'entrée de webhook, le fournisseur vous donne un secret de signature — une chaîne aléatoire que vous et eux seul connaissez. Lorsqu'un événement se déclenche :
- Le fournisseur sérialise le corps de l'événement en JSON (ou une chaîne canonique).
- Il calcule
HMAC-SHA256(secret, payload). - Il envoie la requête avec la signature dans une en-tête —
X-Hub-Signature-256pour GitHub,Stripe-Signaturepour Stripe, et ainsi de suite.
À votre côté, vous effectuez le même calcul sur le corps brut de la requête et vous le comparez. Si les résultats sont identiques, le corps est authentique. Si ce n'est pas le cas, vous le rejetez.
Vérification dans le code
Voici ce que la vérification ressemble dans les langages les plus courants. Le modèle est identique dans tous les cas : calculer, comparer avec une fonction de comparaison à temps constant.
Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be a Buffer or string — NOT parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Python
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
Le détail critique : utiliser le corps brut
L'erreur d'implémentation la plus courante est de passer un payload analysé et re-sérialisé à la fonction HMAC au lieu des octets bruts d'origine. L'ordre des clés, les espaces en blanc et les échappements Unicode influencent tous le hachage. Le fournisseur a signé les octets exacts qu'il a envoyés sur le réseau — vous devez hacher exactement ces mêmes octets.
Dans Express (Node.js), cela signifie configurer le parser de corps pour préserver le tampon brut :
app.use('/webhooks', express.raw({ type: 'application/json' }));
Dans Django, utilisez request.body plutôt que request.data. Dans Flask, utilisez request.get_data().
Pourquoi pas simplement un hachage simple ?
Un hachage simple SHA-256 du payload prouve rien — n'importe qui peut calculer SHA256(payload) sans secret. La clé secrète de l'HMAC est ce qui le rend un authentification code, et non simplement un hachage de contrôle. Il répond à la question « qui m'a envoyé ce message » plutôt qu'à la question « a-t-il été corrompu lors du transit ».
Pourquoi pas des signatures asymétriques (comme RSA) ?
RSA et ECDSA permettent au receveur de vérifier une signature sans connaître la clé privée — c'est une fonction utile pour les transmissions publiques (comme la signature de code). Pour les webhooks, il n'y a que deux parties qui ont besoin de vérifier la signature : vous et le fournisseur. Un secret partagé est plus simple, plus rapide, et tout aussi sécurisé dans ce modèle. Certains fournisseurs (Svix, Clerk) proposent des signatures asymétriques pour les cas où vous ne pouvez pas stocker de secret côté serveur.
Les attaques de répétition — et comment les éviter
Un signature HMAC valide prouve l'authenticité mais pas la fraîcheur. Un attaquant qui capte une requête authentique peut la répéter plus tard. Stripe combat cela en incluant une date dans la Stripe-Signature en-tête et en hachant la date avec le corps de la requête. Sur votre côté, vous rejeterez toute requête dont la date est plus de cinq minutes d'âge.
Si vous construisez votre propre système de webhook, faites de même : incluez un nonce croissant ou un timestamp Unix dans le message signé, et rejeté les requêtes périmées côté serveur.
La comparaison à temps constant n'est pas optionnelle
N'utilisez jamais une comparaison simple des signatures HMAC (===, ==). Une comparaison de chaîne qui court-circuite révèle des informations sur le nombre d'octets correspondants — un attaquant effectuant des milliers de requêtes peut reconstruire la signature attendue octet par octet. Utilisez toujours une comparaison à temps constant :
- Node.js :
crypto.timingSafeEqual() - Python :
hmac.compare_digest() - PHP :
hash_equals() - Go :
hmac.Equal() - Ruby :
ActiveSupport::SecurityUtils.secure_compare()
Résumé : un gestionnaire de webhook de production prêt à l'usage
Voici un exemple complet utilisant l'en-tête X-Hub-Signature-256 de GitHub dans Node.js :
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Keep the body as raw bytes — critical!
app.use('/github/webhook', express.raw({ type: 'application/json' }));
app.post('/github/webhook', (req, res) => {
const sigHeader = req.headers['x-hub-signature-256'];
if (!sigHeader) return res.status(401).send('Missing signature');
const sig = sigHeader.replace('sha256=', '');
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Safe to process the event now
console.log('Event type:', req.headers['x-github-event']);
res.sendStatus(200);
});
app.listen(3000);
Référence rapide : qui utilise quoi
| Fournisseur | En-tête | Algorithme | Date dans la signature ? |
|---|---|---|---|
| GitHub | X-Hub-Signature-256 | HMAC-SHA256 | Non |
| Stripe | Stripe-Signature | HMAC-SHA256 | Oui |
| Shopify | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | Non |
| Twilio | X-Twilio-Signature | HMAC-SHA1 | Non |
| Mou | X-Slack-Signature | HMAC-SHA256 | Oui |
| Paddle | Paddle-Signature | HMAC-SHA256 | Oui |
Vous aimerez peut-être aussi
Installez nos extensions
Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide
恵 Le Tableau de Bord Est Arrivé !
Tableau de Bord est une façon amusante de suivre vos jeux, toutes les données sont stockées dans votre navigateur. D'autres fonctionnalités arrivent bientôt !
Outils essentiels
Tout voir Nouveautés
Tout voirMise à jour: Notre dernier outil a été ajouté le 15 mai 2026
