Webhooks en production : signature HMAC, anti‑rejeu et idempotence

Un webhook est souvent présenté comme une simple URL qui déclenche une automatisation. Techniquement, c’est vrai. Côté sécurité, c’est très incomplet : si cette URL est publique et qu’aucun contrôle sérieux n’est appliqué, n’importe qui peut tenter de lancer le workflow, rejouer un ancien événement ou envoyer une charge malformée.

Une protection robuste ne repose pas sur un « lien secret ». Elle combine plusieurs couches : HTTPS, authentification, signature du corps brut, contrôle temporel, idempotence, validation des données et limitation des ressources.

Le modèle de menace en une minute

Prenons un webhook qui reçoit l’événement invoice.paid et déclenche la création d’une facture, l’envoi d’un message ou l’ouverture d’un accès client. Sans garde-fous, quatre scénarios sont possibles :

  • un tiers découvre l’URL et fabrique une fausse requête ;
  • une requête légitime est modifiée pendant son traitement ou mal interprétée ;
  • un événement valide est renvoyé plusieurs fois, volontairement ou à cause des mécanismes de nouvelle tentative du fournisseur ;
  • un grand nombre de requêtes consomme les exécutions, la mémoire ou les appels vers les services suivants.

Le dernier point est important : un webhook peut être parfaitement authentique et arriver deux fois. Les fournisseurs réessaient souvent lorsqu’ils ne reçoivent pas assez vite une réponse correcte. La sécurité et la fiabilité doivent donc être traitées ensemble.

Commencer par les protections natives de n8n

Le nœud Webhook de n8n peut exiger une authentification Basic, par en-tête ou par jeton JWT. Il propose aussi une liste d’adresses IP autorisées. Lorsque le service émetteur prend en charge l’une de ces méthodes, utilisez-la : elle bloque la requête avant les étapes métier du workflow.

Une liste d’IP n’est utile que si le fournisseur publie des plages stables et les maintient. Elle devient fragile face à des adresses changeantes ou à une infrastructure intermédiaire mal configurée. Elle doit compléter une authentification cryptographique, pas la remplacer.

La protection doit aussi exister sur l’URL de production, pas seulement pendant les tests. n8n sépare les URL de test et de production ; vérifiez que le workflow publié utilise bien les identifiants et les règles prévues.

Ce que prouve une signature HMAC

HMAC combine un message, une fonction de hachage et un secret partagé entre l’émetteur et le destinataire. Avec HMAC‑SHA‑256, l’émetteur calcule une signature à partir du contenu envoyé. Le destinataire refait le même calcul avec son exemplaire du secret :

signature = HMAC_SHA256(secret, message_canonique)

Si les deux signatures correspondent, on peut raisonnablement conclure que la requête a été produite par quelqu’un qui possède le secret et que le message signé n’a pas été modifié. HMAC ne chiffre pas le contenu : HTTPS reste indispensable pour protéger les données pendant le transport.

Pour une nouvelle intégration, utilisez un secret aléatoire suffisamment long — 32 octets aléatoires conviennent à HMAC‑SHA‑256 — et conservez-le dans le gestionnaire d’identifiants ou de secrets de l’environnement. Ne placez jamais ce secret dans l’URL, le dépôt Git ou les journaux.

Signer les bons octets, dans le bon ordre

L’erreur classique consiste à parser le JSON, puis à le reconstruire avant de calculer la signature. Un espace, l’ordre des propriétés ou un retour à la ligne peut changer la suite d’octets sans changer le sens du JSON. La signature ne correspond alors plus.

La vérification doit porter sur le corps brut reçu, avant toute transformation. n8n propose pour cela l’option Raw Body du nœud Webhook. Ensuite, respectez exactement le format défini par le fournisseur. Certains signent uniquement le corps ; d’autres signent une chaîne composée du timestamp, d’un séparateur et du corps brut.

message_canonique = timestamp + "." + corps_brut

Il faut également respecter l’encodage annoncé : hexadécimal, Base64, préfixe sha256=, nom précis de l’en-tête et encodage UTF‑8. Ne recopiez pas l’algorithme d’un autre service sous prétexte qu’il utilise lui aussi HMAC.

Comparer en temps constant

Une comparaison naïve avec === peut s’arrêter dès le premier caractère différent. Dans certains contextes, le temps de réponse révèle alors une petite information sur la signature attendue. Utilisez la fonction de comparaison en temps constant fournie par votre environnement.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifyWebhook(rawBody, timestamp, header, secret) {
  const now = Math.floor(Date.now() / 1000);
  const sentAt = Number(timestamp);

  if (!Number.isFinite(sentAt) || Math.abs(now - sentAt) > 300) {
    return false;
  }

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`, "utf8")
    .digest("hex");

  const received = header.replace(/^sha256=/, "");

  if (!/^[0-9a-f]{64}$/i.test(received)) {
    return false;
  }

  return timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex")
  );
}

Cet exemple illustre le principe, pas un format universel. Le délai de cinq minutes, les noms d’en-têtes et la chaîne signée doivent être adaptés à la documentation de l’émetteur.

Une signature valide peut toujours être rejouée

Si un attaquant récupère une requête légitime complète, il peut la renvoyer avec sa signature valide. HMAC détecte une modification, mais pas automatiquement la réutilisation du même message.

Ajoutez donc deux contrôles :

  1. Une fenêtre temporelle courte : refusez un timestamp trop ancien ou trop éloigné de l’heure du serveur. Surveillez aussi la synchronisation NTP de la machine.
  2. Un identifiant d’événement unique : enregistrez chaque event_id accepté et refusez son traitement métier s’il existe déjà.

L’identifiant doit être marqué de façon atomique, idéalement avec une contrainte unique dans PostgreSQL, Redis ou un stockage équivalent. Le schéma « je vérifie, puis j’insère » sans contrainte laisse une course possible entre deux exécutions simultanées.

Un doublon déjà traité n’est pas forcément une erreur côté fournisseur. On peut répondre 200 ou 204 sans rejouer les actions, afin d’arrêter ses nouvelles tentatives. En revanche, une signature incorrecte doit être refusée avant tout effet métier.

Valider les données après l’authenticité

Une requête authentique peut contenir une erreur ou provenir d’un compte fournisseur compromis. Après la vérification cryptographique, contrôlez encore :

  • le type d’événement attendu ;
  • la présence, le type et la longueur de chaque champ ;
  • les valeurs autorisées et les transitions d’état cohérentes ;
  • le type de contenu HTTP, par exemple application/json ;
  • la taille maximale de la requête ;
  • les droits du workflow sur les systèmes qu’il appelle ensuite.

OWASP recommande de ne jamais faire confiance aux paramètres entrants, d’imposer HTTPS, de limiter la taille des requêtes et d’appliquer un contrôle de débit. Sur n8n autohébergé, la limite globale du corps des webhooks se règle notamment avec N8N_PAYLOAD_SIZE_MAX. Ne l’augmentez pas sans besoin mesuré.

Répondre vite, traiter proprement

Un fournisseur peut renvoyer l’événement s’il attend trop longtemps. Pour une tâche lourde, vérifiez la requête, enregistrez l’événement, placez le travail dans une file ou un traitement séparé, puis répondez rapidement avec un statut adapté, souvent 202 Accepted.

Le traitement suivant doit pouvoir reprendre sans doubler les effets. Une création de facture, un envoi d’e-mail ou une écriture comptable doit toujours être relié à la clé d’idempotence de l’événement d’origine.

La checklist minimale avant mise en production

  • Endpoint disponible uniquement en HTTPS.
  • Authentification native n8n activée lorsque l’émetteur le permet.
  • Corps brut conservé pour la vérification HMAC.
  • HMAC‑SHA‑256 calculé selon la documentation exacte du fournisseur.
  • Comparaison en temps constant.
  • Timestamp contrôlé avec une fenêtre définie.
  • Identifiant d’événement protégé par une contrainte unique.
  • Schéma, type de contenu et taille validés.
  • Limitation de débit et alertes sur les refus répétés.
  • Rotation du secret testée, avec une courte période acceptant l’ancienne et la nouvelle clé.
  • Aucun secret ni corps sensible écrit en clair dans les journaux.
  • Aucune action irréversible avant la fin de tous les contrôles.

En clair

La signature HMAC répond à une question : « ce message provient-il probablement du détenteur du secret et est-il intact ? » Elle ne répond pas à trois autres questions : « est-il récent ? », « a-t-il déjà été traité ? » et « son contenu est-il acceptable ? ».

Un webhook de production sérieux traite les quatre. C’est moins spectaculaire qu’un workflow qui s’allume en deux minutes, mais c’est ce qui évite qu’une automatisation utile devienne un bouton public capable de déclencher des actions sensibles.

Sources

Publications similaires