Les pubs vous déplaisent ? Aller Sans pub Auj.

Signatures d'webhook Vérifier les charges et arrêter les demandes falsifiées

Mis à jour le

Apprenez comment fonctionnent les signatures HMAC-SHA256 des webhooks, comment implémenter une vérification en temps constant en Python, Node.js et PHP, et les erreurs courantes qui provoquent des échecs silencieux en production.

Signatures d'webhook : vérifiez les payloads et arrêtez les requêtes fausses 1
ANNONCE · Supprimer ?

N'importe qui peut envoyer une requête POST vers votre point d'entrée d'webhook. Sans vérification de signature, votre serveur n'a aucune manière de savoir si cette requête provient vraiment de Stripe, de GitHub ou de votre propre infrastructure — ou si elle provient d'un attaquant qui retransmet un événement légitime.

La vérification de la signature d'webhook résout ce problème. C'est la méthode utilisée par les processeurs de paiement, les plateformes de contrôle de version et les systèmes de commerce électronique pour prouver l'authenticité avant d'agir sur les données reçues. Cet article explique comment fonctionnent les signatures HMAC-SHA256, le modèle de vérification étape par étape, ainsi que les erreurs subtiles qui causent des échecs en production.

Pourquoi les webhooks non authentifiés représentent un risque de sécurité

Un point d'entrée d'webhook est simplement un gestionnaire HTTP exposé sur Internet. Sans vérification, toute requête qui y parvient est traitée. Deux types d'attaques se distinguent :

  • Faux-semblant — un attaquant crée un payload fictif qui ressemble à un événement légitime (un paiement réussi, une abonnement renouvelé) et déclenche des actions sur votre côté sans qu'aucune transaction réelle n'ait eu lieu.
  • Attaques de répétition — une requête légitime est capturée en transit et resubmitée plus tard. Si votre point d'entrée est idempotent mais non protégé, le même événement est déclenché plusieurs fois.

Les deux attaques sont prévenues par un secret partagé combiné à une signature cryptographique. L'expéditeur signe le payload ; vous vérifiez la signature avant de traiter quoi que ce soit.

Comment fonctionnent les signatures HMAC-SHA256

L'HMAC (Code d'authentification basé sur le hash) prend deux entrées : une clé secrète et un message. Il les passe par une fonction de hachage — SHA-256 dans la plupart des implémentations d'webhook — et produit une signature de longueur fixe.

Propriété clé : le même secret + message produisent toujours la même signature, et tout changement d'un seul octet dans le message produit une sortie complètement différente. Quiconque n'a pas la clé secrète ne peut pas produire une signature valide, même s'il peut voir le payload entier.

En pratique, le flux ressemble à ce qui suit :

  1. Vous inscrivez votre webhook auprès d'un service (par exemple Stripe). Ce service vous donne une secret de signature.
  2. Lorsque le service envoie un événement, il calcule HMAC-SHA256(secret, payload) et inclut le résultat dans une en-tête de requête.
  3. Votre serveur reçoit la requête, calcule le même HMAC en utilisant votre clé secrète et le corps brut de la requête, puis compare les deux signatures.
  4. Si elles sont identiques, la requête est authentique. Si elles ne le sont pas, elle est rejetée.

Le modèle de vérification, étape par étape

Voici une implémentation en Python qui correspond à ce que chaque service principal attend :

import hmac
import hashlib

def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=payload,
        digestmod=hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received_sig)

Deux points sont essentiels ici au-delà de l'appel HMAC lui-même. Premièrement : payload est bytes, pas une chaîne décodée — vous devez passer le corps brut de la requête exactement tel qu'il a été reçu du réseau. Deuxièmement : la comparaison utilise hmac.compare_digest au lieu de ==. C'est intentionnel.

Pourquoi une comparaison en temps constant n'est pas optionnelle

La comparaison de chaînes dans la plupart des langages court-circuite : elle retourne false au premier caractère qui ne correspond pas. Un attaquant qui peut mesurer le temps de réponse peut exploiter cela — en envoyant des milliers de requêtes avec des signatures variées et en utilisant les différences de temps pour deviner la valeur correcte caractère par caractère. C'est une attaque de temps.

hmac.compare_digest en Python, hash_equals en PHP, et crypto.timingSafeEqual en Node.js prennent tous la même durée, quel que soit le point où les chaînes diffèrent. Utilisez-les chaque fois que vous comparez une signature — sans exception.

Formats d'en-tête réels : Stripe, GitHub, Shopify

Chaque service a un nom d'en-tête légèrement différent et une méthode d'encodage de la signature. Voici ce que vous devez analyser pour les trois intégrations les plus courantes.

Stripe

Stripe envoie une Stripe-Signature en-tête avec ce format :

Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe

Le t le champ est l'heure Unix à laquelle l'événement a été envoyé. La v1 valeur est le HMAC-SHA256 de timestamp + "." + raw_body. Lorsqu'on l'implémente manuellement :

  1. Analysez l'en-tête pour extraire t et v1.
  2. Calculez HMAC-SHA256(secret, t + "." + raw_body).
  3. Comparez avec v1 en utilisant une comparaison en temps constant.
  4. Rejetez si t est supérieure à 300 secondes (5 minutes) à l'heure actuelle.

GitHub

GitHub utilise l' X-Hub-Signature-256 en-tête :

X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3

Retirez le préfixe sha256= puis comparez le reste contre votre HMAC du corps brut. GitHub ne fait pas d'encodage de timestamp dans le contenu signé, donc implémentez la déduplication des événements séparément si la protection contre les répétitions est importante pour votre cas d'usage.

Shopify

Shopify utilise X-Shopify-Hmac-Sha256 avec le digest encodé en Base64 plutôt qu'en hexadécimal :

X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=

Décodez les deux valeurs HMAC et l'en-tête de Base64 en octets bruts, puis comparez avec une fonction de comparaison en temps constant. Comparer les chaînes en Base64 directement peut introduire des bugs subtils liés à la normalisation de l'encodage.

L'erreur la plus courante : corps brut versus JSON décodé

C'est là où la plupart des échecs de vérification de signature d'webhook se produisent en production. Lorsque votre framework décode automatiquement le corps de la requête en un dictionnaire ou objet avant que votre gestionnaire ne s'exécute, les octets bruts sont perdus. L'HMAC a été calculé sur ces octets originaux — pas sur ce que produit votre bibliothèque JSON lorsqu'elle re-encode le modèle décodé.

Même si les données sont sémantiquement identiques, {"amount": 100} et {"amount":100} (sans espace après la virgule) produisent des valeurs HMAC différentes. Les différences d'ordre des champs, la normalisation Unicode et la précision des flottants peuvent tous briser la vérification sans erreur évidente.

Mettez également à jour la configuration de la connexion à la base de données de votre application. Dans MySQL PDO : Bufferez le corps brut de la requête avant toute décomposition et transmettez ces octets à votre fonction de vérification. Dans Express.js, inscrivez le route avec express.raw() middleware plutôt que express.json():

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body; // Buffer — not a parsed object
  const sig = req.headers['stripe-signature'];
  // Verify rawBody against sig before JSON.parse()
});

Dans Django, utilisez request.body (octets). Dans Laravel, utilisez $request->getContent(). Le modèle est cohérent : accédez directement aux octets bruts du requête, sans jamais re-serialiser une représentation décodée.

Protection contre les répétitions avec des timestamps

Une signature valide prouve seulement que le payload n'a pas été altéré en transit. Elle ne prévient pas un attaquant de capturer une requête légitime et de la resubmettre plusieurs heures plus tard — la signature sera toujours valide car rien n'a changé.

Stripe résout cela en incluant un timestamp Unix dans le payload signé et en recommandant une fenêtre de tolérance de 5 minutes. Après avoir extrait t de l' Stripe-Signature en-tête, rejetez les requêtes où le timestamp est en dehors de cette fenêtre :

import time

def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
    return abs(time.time() - timestamp) <= tolerance_seconds

# In your handler:
if not is_within_tolerance(stripe_timestamp):
    return HttpResponse(status=403)  # Reject stale request

Pour les services comme GitHub et Shopify qui n'incorporent pas de timestamp dans le schéma de signature, implémentez une protection contre les répétitions en stockant les IDs d'événements (disponibles dans le payload) et en rejetant tout ID que vous avez déjà traité. Un cache à durée de vie limitée ou un ensemble Redis avec une durée de vie correspondant à votre fenêtre de traitement fonctionne bien.

Vérifiez les signatures sans écrire de code

Lorsque vous déboguez une intégration webhook — ou que vous auditez un payload que vous avez déjà reçu — le Webhook Signature Validator vous permet de coller un payload, une clé secrète et une signature reçue pour vérifier immédiatement la validité, sans avoir à démarrer un environnement local.

Pour générer des signatures HMAC pour tester votre point d'entrée avec des payloads artificiels, utilisez le Générateur HMAC. Il produit des sorties en hexadécimal et en Base64 pour SHA-256, SHA-512 et plusieurs autres algorithmes de hachage — utile pour construire des cas de test couvrant les formats utilisés par chaque service.

Référence Rapide

  • L'HMAC-SHA256 avec une clé partagée est le schéma standard de signature utilisé par Stripe, GitHub, Shopify et la plupart des autres services.
  • Utilisez toujours une comparaison en temps constant (hmac.compare_digest, hash_equals, crypto.timingSafeEqual) — pas ==.
  • Transmettez les octets bruts du corps de la requête à votre fonction HMAC, jamais un objet re-serialisé en JSON.
  • Vérifiez le timestamp sur les services qui l'incluent ; la fenêtre de tolérance de Stripe est de 5 minutes.
  • Déduisez par ID d'événement pour les services sans protection contre les répétitions basée sur le timestamp.
  • Le nom de l'en-tête et l'encodage (hexadécimal vs Base64) varient selon le service — vérifiez toujours les documents d'intégration.
Envie d'une expérience sans pub ? Passez à la version sans pub

Installez nos extensions

Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide

Sur Extension Chrome Sur Extension de bord Sur Extension Firefox Sur Extension de l'opéra

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 !

ANNONCE · Supprimer ?
ANNONCE · Supprimer ?
ANNONCE · Supprimer ?

Coin des nouvelles avec points forts techniques

Impliquez-vous

Aidez-nous à continuer à fournir des outils gratuits et précieux

Offre-moi un café
ANNONCE · Supprimer ?