Comment bloquer un webhook forgé sans signature vérifiée
Section intitulée « Comment bloquer un webhook forgé sans signature vérifiée »Un webhook est une requête HTTP (Hypertext Transfer Protocol) envoyée par un service vers une application lorsqu’un événement survient. Si l’application accepte cette requête sur la seule foi de son URL, de son adresse IP ou d’un champ JSON (JavaScript Object Notation), elle accepte aussi celle d’un attaquant qui imite l’événement.
Un webhook ressemble à un livreur qui dépose un ordre dans une boîte aux lettres : « expédier ce colis », « rembourser cette commande », « créer cet accès ». Si personne ne vérifie qui a déposé le papier, n’importe qui peut glisser le même ordre et faire agir l’entreprise à sa place.
→ Lire l’explication complète de la marmotte
Webhook forgé : le mécanisme et l’impact
Section intitulée « Webhook forgé : le mécanisme et l’impact »Un webhook forgé consiste à envoyer directement une requête vers l’endpoint de réception en reproduisant la structure attendue : méthode HTTP, en-têtes, corps JSON et identifiant d’événement. L’attaquant n’a pas besoin de compromettre le fournisseur émetteur. Il cible le récepteur, souvent exposé publiquement, qui traite {"status":"paid"} ou {"action":"user.created"} comme une vérité révélée.
La vulnérabilité apparaît quand l’application déduit l’identité de l’émetteur d’un attribut contrôlable ou insuffisant :
- une adresse IP source, fragile derrière un proxy, un réseau de diffusion ou une plage fournisseur mouvante ;
- un en-tête
User-Agent, qui n’est qu’une chaîne de caractères ; - un champ
source,providerouevent_typedu JSON ; - une URL longue présentée comme secrète ;
- une authentification HTTP Basic dont le mot de passe est partagé entre tous les émetteurs ;
- une signature reçue mais jamais vérifiée, ce qui mérite une place spéciale dans le musée des contrôles décoratifs.
Une URL non publiée réduit le bruit opportuniste, pas la capacité d’un attaquant déterminé. Elle fuit dans les journaux de reverse proxy, les traces d’erreur, les outils de supervision, les captures de requêtes et parfois dans une capture d’écran qui avait pourtant été floutée avec une confiance cosmique. Une URL est un emplacement, pas une preuve cryptographique.
L’impact dépend de l’action déclenchée. Les cas les plus fréquents sont le passage frauduleux d’une commande à l’état payé, le remboursement indu, la création ou l’élévation d’un compte, l’exécution d’un pipeline de livraison, ou l’import de données depuis une URL imposée. Un webhook de dépôt de code qui lance une compilation sur un runner doté de secrets transforme rapidement une petite route HTTP en distributeur de privilèges.
La frontière à protéger est donc précise : prouver que le corps exact reçu a été produit par un émetteur autorisé, puis prouver qu’il est récent et non déjà consommé. La surface d’attaque des services exposés ne s’arrête jamais au pare-feu. Une requête autorisée à atteindre un endpoint n’est pas autorisée à commander un effet métier.
Démonstration de requête webhook forgée en laboratoire
Section intitulée « Démonstration de requête webhook forgée en laboratoire »Le laboratoire suivant utilise Python 3.11 et Flask 3.0. Il expose deux routes locales : l’une accepte aveuglément un événement, l’autre exige une signature HMAC (Hash-based Message Authentication Code) calculée sur le corps brut et un horodatage. Le secret est fixé dans l’environnement uniquement pour la démonstration.
# Prérequis : Python 3.11, pip install "flask==3.0.*"import hashlibimport hmacimport osimport timefrom flask import Flask, abort, request
app = Flask(__name__)WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "").encode()
@app.post("/webhook/insecure")def insecure(): # Vulnérable : tout client peut choisir l'état métier. event = request.get_json(force=True) return {"accepted": True, "status": event["status"]}, 200
@app.post("/webhook/verified")def verified(): # Lire les octets avant toute désérialisation ou normalisation JSON. raw_body = request.get_data(cache=True) timestamp = request.headers.get("X-Webhook-Timestamp", "") received = request.headers.get("X-Webhook-Signature", "")
try: age = abs(int(time.time()) - int(timestamp)) except ValueError: abort(400, "horodatage invalide")
if age > 300: abort(401, "evenement expire")
signed_payload = timestamp.encode() + b"." + raw_body expected = hmac.new( WEBHOOK_SECRET, signed_payload, hashlib.sha256 ).hexdigest()
# compare_digest évite une comparaison à durée variable. if not hmac.compare_digest(expected, received): abort(401, "signature invalide")
event = request.get_json(force=True) return {"accepted": True, "status": event["status"]}, 200Démarrer le serveur avec un secret distinct de tout mot de passe humain :
export WEBHOOK_SECRET='lab-secret-ne-pas-reutiliser'flask --app app run --host 127.0.0.1 --port 5000
# Cette requête forge un événement accepté par la route vulnérable.curl -sS -X POST http://127.0.0.1:5000/webhook/insecure \ -H 'Content-Type: application/json' \ --data '{"event":"payment.updated","status":"paid","amount":1}'La réponse contient accepted: true. Aucune identité de l’émetteur n’a été établie. Modifier amount, status ou un identifiant de commande ne demande aucune cryptanalyse, seulement un client HTTP. C’est rarement la partie qui occupe les attaquants le plus longtemps.
La même charge est rejetée par /webhook/verified sans X-Webhook-Signature. Pour produire une requête légitime en laboratoire, il faut signer exactement les octets transmis :
body='{"event":"payment.updated","status":"paid","amount":1}'ts=$(date +%s)sig=$(printf '%s.%s' "$ts" "$body" | \ openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex | sed 's/^.* //')
curl -sS -X POST http://127.0.0.1:5000/webhook/verified \ -H 'Content-Type: application/json' \ -H "X-Webhook-Timestamp: $ts" \ -H "X-Webhook-Signature: $sig" \ --data "$body"Ce code ne met pas en œuvre la détection durable de rejeu. Il démontre l’authenticité et la fraîcheur, pas l’unicité de traitement. Rejouer la même requête dans les cinq minutes reste possible tant qu’aucun identifiant d’événement n’est conservé. C’est volontaire ici : un cache mémoire déguisé en anti-fraude ne mérite pas une médaille, surtout après le premier redémarrage.
Signature HMAC, signature asymétrique et rejeu
Section intitulée « Signature HMAC, signature asymétrique et rejeu »La signature HMAC utilise un secret partagé entre l’émetteur et le récepteur. L’émetteur calcule une empreinte authentifiée sur les octets du message, et le récepteur recalcule cette valeur avec le même secret. Sans secret, un attaquant ne peut pas fabriquer une signature valide avec une probabilité praticable.
Le compromis central est net : HMAC coûte peu, se vérifie vite et convient aux intégrations bilatérales, mais le récepteur possède une clé qui peut aussi signer. Une signature asymétrique sépare ces rôles : l’émetteur détient une clé privée, le récepteur ne conserve qu’une clé publique de vérification. Elle coûte davantage en calcul et en gestion de clés, mais évite que chaque consommateur puisse usurper l’émetteur.
La recommandation est tranchée : utiliser le mécanisme officiellement pris en charge par le fournisseur, en préférant HMAC pour une intégration point à point avec secret dédié, et une signature asymétrique lorsque plusieurs consommateurs indépendants vérifient le même flux ou lorsque la distribution du secret partagé devient un risque. Inventer un format de signature maison pour « aller plus vite » revient généralement à financer une migration lente plus tard.
La vérification doit porter sur le corps brut, avant toute conversion JSON. Parser puis re-sérialiser le document casse la signature dès qu’un espace, l’ordre des propriétés ou l’encodage Unicode diffère. Une signature sécurise des octets, pas l’interprétation philosophique qu’une bibliothèque choisit d’en faire.
L’horodatage limite la fenêtre de rejeu, typiquement à 300 secondes. Il ne suffit pas pour les événements à effet non idempotent, c’est-à-dire ceux dont une seconde exécution produit un effet supplémentaire. Il faut alors stocker durablement l’identifiant unique de l’événement après une vérification réussie, avec une contrainte d’unicité en base de données. La transaction qui inscrit cet identifiant et celle qui déclenche l’effet métier doivent être atomiques, ou l’attaquant peut exploiter une course entre deux livraisons concurrentes.
L’idempotence doit aussi exister côté métier. Un identifiant de webhook dédupliqué ne protège pas contre deux événements légitimes distincts visant la même facture. Une transition d’état doit vérifier son état antérieur : accepter pending vers paid, refuser paid vers paid si l’opération est comptable, et journaliser la décision.
Durcissement de l’endpoint de réception
Section intitulée « Durcissement de l’endpoint de réception »La signature est nécessaire, mais elle ne transforme pas l’endpoint en bunker orbital. Le transport TLS (Transport Layer Security) protège la confidentialité et l’intégrité entre l’émetteur et le point de terminaison TLS. Il ne remplace pas la signature applicative : un proxy interne compromis, une mauvaise terminaison TLS ou un journal de requêtes peuvent encore affecter le flux après déchiffrement.
Placer l’endpoint derrière un reverse proxy, limiter les méthodes à POST, plafonner la taille du corps et imposer Content-Type: application/json réduit les angles inutiles. Ces contrôles ne doivent jamais précéder logiquement la signature métier : filtrer une plage IP fournisseur est une défense complémentaire, pas une authentification. Les plages changent, les proxy existent, et l’Internet adore rappeler qu’une liste statique est une promesse faite à l’entropie.
Conserver une liste fermée d’algorithmes autorisés. Si le fournisseur annonce sha256, refuser none, md5, les préfixes ambigus et toute négociation issue d’un en-tête contrôlé par l’émetteur. La clé de sélection doit venir de la configuration locale ou d’un identifiant de clé explicitement associé à une politique connue.
La validation de schéma intervient après l’authentification cryptographique. Vérifier les types, les champs obligatoires, les bornes numériques et les identifiants attendus. Ne jamais faire suivre directement un champ webhook dans une requête interne, une commande, une URL de téléchargement ou une règle d’autorisation. La signature prouve l’origine du message, pas l’absence de bogue chez l’émetteur ni la sûreté de ses données.
Les dépendances entre identité, réseau et secrets sont détaillées dans les pages architecture cybersécurité et gestion des identités. Un webhook est une frontière de confiance, même lorsqu’il porte un joli logo SaaS.
Exploitation : secrets, rotation et supervision
Section intitulée « Exploitation : secrets, rotation et supervision »Le secret HMAC doit être unique par fournisseur, environnement et endpoint logique. Réutiliser la même valeur pour trois intégrations transforme la compromission d’un consommateur secondaire en capacité de forger tous les autres flux. Stocker ce secret dans un gestionnaire de secrets, jamais dans le dépôt, l’image de conteneur ou une variable affichée par une interface de pipeline.
La rotation doit accepter deux secrets actifs durant une fenêtre bornée : l’ancien pour vérifier les livraisons déjà en transit, le nouveau pour les livraisons récentes. Chaque secret porte un identifiant de version localement journalisé. Une rotation sans recouvrement provoque des pertes ; un recouvrement éternel transforme une fuite ancienne en abonnement à durée indéterminée.
Une clé de chiffrement maîtresse, ou KEK (Key Encryption Key), peut protéger les secrets stockés dans un coffre. Un HSM (Hardware Security Module) protège les opérations cryptographiques et les clés qu’il héberge, mais ne corrige pas une application qui valide une signature sur le mauvais champ. Le matériel ne lit pas les tickets d’incident, malgré ses prétentions silencieuses.
Journaliser l’identifiant d’événement, l’émetteur attendu, le résultat de vérification, le motif de rejet et une empreinte du corps, pas le corps complet s’il contient des données personnelles ou des secrets. Compter séparément les signatures invalides, les horodatages expirés, les rejoues et les erreurs de schéma. Une hausse de rejets cryptographiques justifie une alerte rapide ; une hausse d’erreurs de schéma peut signaler un changement fournisseur ou une tentative d’exploration.
Prévoir une file durable après vérification pour découpler la réception de l’effet métier. La réponse HTTP doit confirmer la réception authentifiée, puis le traitement doit être rejouable de façon idempotente. Les sauvegardes doivent inclure le registre des identifiants déjà consommés lorsque son absence permettrait de rejouer des opérations sensibles après restauration.
🐝 Ce qui arrive si on s’en passe
Section intitulée « 🐝 Ce qui arrive si on s’en passe »Sans vérification de signature, le journal des webhooks perd toute valeur probante : un événement forgé y figure avec le même format, le même statut et la même réponse 200 qu’une livraison légitime. Aucune recherche ne permet ensuite de séparer les vrais ordres des faux. Aucun compteur de rejets cryptographiques n’existe non plus : les essais successifs d’un attaquant qui ajuste montants et identifiants restent donc invisibles jusqu’à l’effet métier. La reprise après incident devient une réconciliation complète. Chaque commande, remboursement ou compte créé depuis la mise en service doit être comparé à l’historique consultable chez le fournisseur, car la base locale ne peut plus servir de référence. Tant que cette comparaison n’est pas terminée, ni la comptabilité ni les droits d’accès ne peuvent être considérés comme exacts.
🦫 Explication de la marmotte 🍫
Section intitulée « 🦫 Explication de la marmotte 🍫 »Un webhook est un message envoyé par un service à une application pour signaler un événement, par exemple un paiement ou la création d’un compte. L’application reçoit ce message sur une adresse accessible sur Internet. Le problème commence si elle croit le contenu sans vérifier qui l’a envoyé : une personne malveillante peut envoyer le même type de message avec de fausses informations.
Pour empêcher cela, l’émetteur et le récepteur possèdent un secret commun, différent pour chaque liaison. L’émetteur calcule une preuve à partir du contenu exact du message, de la date d’envoi et de ce secret. Il transmet le message, la date et la preuve. Le récepteur récupère d’abord les données telles qu’elles sont arrivées, puis calcule sa propre preuve avec le même secret. Si les deux preuves diffèrent, le message est rejeté avant toute action.
La date limite l’utilisation d’un ancien message volé. Mais elle ne suffit pas : pendant une courte durée, le même message peut encore être renvoyé. Le récepteur conserve donc l’identifiant de chaque événement déjà traité et refuse une seconde réception du même identifiant.
Le secret doit rester hors du code et être remplacé régulièrement. Les refus doivent être surveillés. Enfin, même un message authentique doit être contrôlé : son contenu peut être incomplet, incohérent ou déclencher une action interdite.
À lire aussi
- Sécuriser la télémétrie et les données d’usage produitConcevoir une collecte de télémétrie produit minimisée, consentie, protégée et supprimée selon une durée de rétention vérifiable.
- Qualité des données et erreurs silencieusesLa qualité des données devient un sujet de sécurité quand une erreur silencieuse modifie une décision, un droit ou une exposition sans déclencher d’alerte.
- Comment minimiser les données collectéesLa minimisation des données réduit la surface d’attaque en limitant la collecte, la conservation et la diffusion des informations personnelles.
- Attaque par confusion de scope OAuthDémonstration d’une élévation de périmètre OAuth causée par des scopes trop larges et exploitation d’un jeton d’accès compromis.
Pages voisines
La citent
Comment bloquer un webhook forgé sans signature vérifiée
Elle cite
: les deux pages se citent l’une l’autre.
