InstaTunnel
Basculer le menu de navigation
API
13 min read
44 views

Signé, Scellé, Manglé : Pourquoi les signatures webhook échouent derrière un tunnel

La signature webhook échoue derrière un tunnel ? Le corps brut, la ré-serialisation JSON, la tolérance d'horodatage et la signature d'URL Twilio perturbent les vérifications HMAC. Voici comment y remédier.

IT
InstaTunnel Team
Published by the InstaTunnel team | Editorial policy
Signé, Scellé, Manglé : Pourquoi les signatures webhook échouent derrière un tunnel

Quick answer

Signatures webhook échouent-elles derrière un tunnel ? Voici la solution: quick comparison answer

Choose the tunnel tool based on the network model: public HTTPS URLs for webhooks and demos, private mesh access for internal apps, and managed infrastructure when policy controls matter most.

Which tunnel tool is best for public webhook testing?

Use a public HTTPS localhost tunnel with stable URLs. InstaTunnel focuses on webhook testing, demos, OAuth callbacks, and MCP endpoint workflows.

When should I choose a private network tool instead?

Choose a private mesh or Zero Trust tool when every user and service should stay inside a controlled private network.

Vous dirigez un tunnel vers localhost:3000, envoyez un événement de test, et votre gestionnaire répond 400 Invalid signature. La charge utile semble correcte. Le secret semble correct. Alors pourquoi la vérification de la signature webhook échoue-t-elle seulement lorsque la requête traverse un tunnel ou un reverse proxy ?

Une signature est une promesse sur des octets exacts, un timing exact et, pour certains fournisseurs, une URL exacte. Un tunnel, un proxy ou un middleware utile peuvent modifier l’un de ces éléments alors que la requête paraît toujours saine. Ce guide couvre les trois familles d’échecs sur lesquelles les développeurs tombent le plus souvent : corps brut versus JSON ré-serialisé, tolérance d’horodatage, et la signature d’URL au style Twilio. Il se termine par une checklist de débogage et un aperçu des outils de débogage disponibles à partir de novembre 2026.

Comment fonctionnent les signatures webhook

Presque tous les fournisseurs utilisent un HMAC : l’expéditeur combine un secret partagé avec un « contenu signé », le hache et place le résultat dans un en-tête. Le récepteur reconstruit le même contenu, calcule son propre HMAC et compare les deux en temps constant. Les fournisseurs diffèrent sur ce qui entre dans le contenu signé, ainsi que sur l’algorithme de hachage et l’encodage.

Fournisseur En-tête Contenu signé Algorithme
Stripe Stripe-Signature (t=…,v1=…) {timestamp}.{raw body} HMAC-SHA256, hex
GitHub X-Hub-Signature-256 (sha256=…) raw payload only HMAC-SHA256, hex
Slack X-Slack-Signature (v0=…) v0:{timestamp}:{raw body} HMAC-SHA256, hex
Webhooks Standard (Svix, Resend, Clerk, Polar, OpenAI et autres) webhook-signature (v1,…) {webhook-id}.{webhook-timestamp}.{raw body} HMAC-SHA256, Base64
Twilio X-Twilio-Signature URL complète + paramètres POST triés HMAC-SHA1, Base64

Stripe, Slack et Webhooks Standard intègrent un horodatage dans la signature. GitHub signe le corps brut sans horodatage. Twilio signe l’URL. Chaque design donne à un tunnel une manière différente de casser les choses.

Échec n°1 : corps brut versus JSON ré-serialisé

La cause la plus fréquente d’un « décalage de signature » n’est pas un secret incorrect. C’est un parseur JSON qui a été exécuté avant votre code de vérification.

Stripe signe le corps de la requête brut et non modifié, et la plupart des frameworks web analysent le corps et jettent les octets d’origine. La documentation de Slack souligne le même point : la forme brute du corps, y compris les espaces, est ce que Slack utilise pour calculer la signature.

À quoi ressemble le “ mangé ”

Si vous parsez JSON puis appelez JSON.stringify(req.body) pour alimenter le HMAC, vous vérifiez une chaîne différente de celle signée par le fournisseur. La ré-serialization peut changer :

  • Les espaces et l’ordre des clés, qui ne concordent que rarement byte par byte avec l’original.
  • L’échappement Unicode : un émetteur peut écrire "\u00e9" là où votre sérialiseur écrit "é". Les deux représentent la même valeur mais avec des octets différents.
  • Le formatage des nombres : en JavaScript, 1.0 devient 1, et les entiers dépassant 2^53 perdent de la précision.
  • Le chiffrement des caractères : les conseils de GitHub recommandent de traiter les charges utiles en UTF-8, car elles peuvent contenir des caractères unicode.

Chacun de ces éléments produit un objet qui semble valide mais dont la signature est invalide.

Le piège Express

En Express, l’ordre des middlewares décide de tout, car quel que soit le parser de corps qui s’exécute en premier consomme le flux de requête. C’est cassé :

app.use(express.json());                                   // s’exécute en premier; req.body est maintenant un objet
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), handler);

La route avec le parser raw ne reçoit rien. Enregistrez les routes webhook avec express.raw() avant le parser JSON global, ou capturez les octets avec le hook verify :

app.use(express.json({
  verify: (req, _res, buf) => { req.rawBody = buf; }   // garder les octets exacts
}));

Hash req.rawBody, jamais JSON.stringify(req.body). Une garde Buffer.isBuffer(req.body) dans une route raw transforme un faux “signature mismatch” en une erreur de configuration évidente.

Où le tunnel intervient

Un tunnel qui ne relaie que les requêtes HTTP devrait livrer les mêmes octets de corps qu’il a reçus. Le problème survient lorsque quelque chose dans le chemin transforme la requête : un proxy qui réécrit le corps, un middleware qui normalise le contenu, ou une couche d’inspection qui ré-encode. Si la vérification réussit quand vous appelez directement l’application mais échoue via le tunnel, comparez les octets bruts que votre application reçoit avec ceux envoyés par le fournisseur.

Les inspecteurs de tunnel peuvent aider ici, avec une précaution : l’Inspector de trafic de ngrok, par exemple, ne collecte que les métadonnées de requête par défaut pour éviter d’exposer les secrets. Activez la capture complète dans vos paramètres de compte avant qu’il n’enregistre les corps complets des requêtes et des réponses.

Un autre piège de développement local : le secret de signature dépend de l’origine de l’événement. Le guide de dépannage de Stripe indique l’erreur de vérification la plus courante est l’utilisation du mauvais secret de point de terminaison. À la fois l’endpoint du Dashboard et le Stripe CLI produisent des secrets qui commencent par whsec_, mais ils sont différents. Ne pas vérifier les événements redirigés CLI avec le secret d’un endpoint du Dashboard, ou l’inverse. Aucun traitement du corps ne résoudra ce problème.

Échec n°2 : tolérance d’horodatage

Les signatures horodatées existent pour prévenir les attaques par réutilisation. Un attaquant qui capture une requête valide peut la renvoyer avec sa signature intacte, les destinataires vérifiant aussi que l’horodatage est récent. La valeur par défaut la plus courante est de cinq minutes :

  • Les bibliothèques Stripe rejettent les événements dont l’horodatage s’écarte de plus de 300 secondes par défaut, et permettent de surmonter la tolérance.
  • Slack recommande de rejeter les requêtes dont l’horodatage X-Slack-Request-Timestamp diffère de l’heure locale de plus de cinq minutes.
  • Les Webhooks standards utilisent par défaut une tolérance de 300 secondes. Certaines implémentations vérifient dans les deux sens et rejettent les horodatages futures aussi bien que les horodatages périmés.

Pour Webhooks standards, webhook-id reste le même lors des retries pour le même événement, tandis que webhook-timestamp enregistre quand chaque tentative a été signée. L’horodatage est donc frais à chaque retry côté fournisseur.

GitHub est l’exception. Sa signature ne porte pas d’horodatage, il n’y a donc pas de vérification de fraîcheur. Dédupliquez avec l’ID X-GitHub-Delivery à la place.

Pourquoi les tunnels rendent les échecs d’horodatage plus probables

  1. Dérive d’horloge sur les machines de dev. Un ordinateur portable qui s’est mis en veille ou une VM ou un conteneur avec une horloge décalée peut être en retard de plusieurs minutes. Hors tolérance, chaque requête échoue même si la signature est correcte. Synchronisez l’horloge d’abord.
  2. Rejouer des événements capturés. Les inspecteurs de tunnel vous permettent de renvoyer une requête capturée, y compris avec des modifications. Ngrok offre à la fois un simple replay et un “replay with modifications”. Un événement Stripe datant d’il y a plusieurs heures échouera la vérification; re-signez avec un horodatage frais, ou élargissez la tolérance en configuration de développement uniquement.
  3. Livraison retardée en transit. Les rejets du fournisseur sont re-signés, ce qui n’est pas le problème. Une seule livraison que votre tunnel laisse tomber, met en file d’attente ou retarde au-delà de la fenêtre de tolérance est le problème.

En production, gardez la tolérance par défaut de 300 secondes et ne désactivez pas la vérification en dehors d’un test éphémère. Enregistrez l’écart d’horloge calculé à chaque rejet afin que « horodatage périmé » et « mauvaise signature » apparaissent comme des erreurs distinctes. Les instructions Slack traitent aussi la vérification de l’horodatage comme une étape distincte avant la comparaison des signatures.

Astuce Webhooks Standard

La clé de signature Webhooks Standard est normalement le secret dont le préfixe whsec_ a été retiré et le reste décodé en Base64. Tous les fournisseurs ne suivent pas cela à la lettre. La documentation de Polar indique que Polar utilise la chaîne secrète complète, y compris le préfixe, comme clé HMAC brute. Si vous vérifiez manuellement plutôt que d’utiliser une bibliothèque, consultez la docs de votre fournisseur.

Échec n°3 : Twilio signe l’URL complète

Le schéma de Twilio surprend le plus les gens, car le corps n’est qu’une partie de ce qui est signé. Selon la documentation de Twilio :

  1. Commencez par l’URL complète de la requête, du protocole jusqu’à la fin de la chaîne de requêtes.
  2. Pour un POST encodé en formulaire, triez les paramètres POST par ordre alphabétique de nom et ajoutez chaque nom et valeur à l’URL, sans délimiteurs.
  3. Calculez un HMAC-SHA1 de cette chaîne, en utilisant comme clé votre Auth Token.
  4. Encodez le résultat en Base64 et comparez-le à X-Twilio-Signature.

Puisque l’URL est incrustée, un proxy qui réécrit le schéma, l’hôte ou le chemin casse la validation. La requête est authentique, mais votre code reconstruit une chaîne différente de celle que Twilio a hachée.

Les helpers de validation de Twilio attendent également que vous passiez tous les paramètres reçus, pas une liste codée en dur, car Twilio peut ajouter des paramètres à tout moment. Les paramètres de la chaîne de requête font déjà partie de l’URL, ne les passez donc pas séparément non plus.

Ce qui casse l’URL derrière un tunnel

Les conseils d’assistance de Twilio indiquent que la cause principale des 403 derrière un proxy est presque toujours une discordance entre l’URL signée par Twilio et celle que votre application reconstruit. Les suspects habituels :

  • Mauvais schéma. TLS se termine à l’extrémité, donc votre application voit http:// alors que Twilio a signé https://. Récupérez le schéma d’origine avec X-Forwarded-Proto, comme l’explique le guide Node.js de Twilio sur la terminaison SSL.
  • Mauvais hôte. Votre application voit un nom d’hôte interne ou localhost:3000, mais Twilio a signé le nom d’hôte public du tunnel. Si quelque chose dans le chemin réécrit l’en-tête Host, n’utilisez pas cela pour reconstruire l’URL signée.
  • Chemin ou chaîne de requête manquants. Des objets de framework comme request.url ou request.host peuvent renvoyer une URL partielle, et reconstruire https://host sans le chemin modifie la chaîne signée.
  • Barre oblique finale. La valeur signée est une chaîne simple, donc /webhook et /webhook/ produisent des signatures différentes. Faites correspondre l’URL telle qu’elle apparaît exactement dans la Twilio Console.
  • Ports. Le validateur twilio-ruby vérifie la signature avec et sans port explicite, avec un commentaire de code indiquant que la génération de signature côté Twilio est incohérente à ce sujet. Si vous implémentez vous-même la vérification, testez les deux formes.
  • Nom d’hôte qui change. Si votre tunnel attribue un nouveau nom d’hôte à chaque session, mettez à jour l’URL dans Twilio et dans votre validateur. Une valeur périmée échoue à chaque requête.

Corps JSON et bodySHA256

Tous les webhooks Twilio ne sont pas codés en formulaire. Pour des requêtes application/json, Twilio ajoute un paramètre de requête bodySHA256 à l’URL de votre webhook, qui contient un hash SHA-256 du corps brut. Les SDK de Twilio exécutent alors deux vérifications : le hash du corps doit correspondre au paramètre, et la signature, calculée sur l’URL incluant ce paramètre, doit être valide. Un middleware de manglage du corps échoue donc la vérification du hash même si votre logique d’URL est parfaite. Dans Node, c’est le travail de validateRequestWithBody. Conservez le corps brut ici aussi.

La solution la plus sûre

  1. Fiez-vous uniquement aux en-têtes de forwarding d’un proxy que vous contrôlez. Reconstruisez l’URL à partir de X-Forwarded-Proto et X-Forwarded-Host. Cela ne fonctionne que si un proxy de confiance les définit réellement, et il ne faut jamais les prendre en compte depuis Internet ouvert. Dans Express, cela signifie aussi configurer trust proxy correctement pour que req.protocol reflète le schéma transmis.
  2. Codez en dur l’URL publique. Vous savez déjà ce que vous avez configuré dans Twilio. Validez contre cette constante. C’est ennuyeux, et cela élimine toute la classe de bugs de reconstruction.

Le schéma de Twilio n’inclut pas d’horodatage, il n’existe donc pas de fenêtre de replay intégrée. Si les replays comptent, dédupliquez sur des identifiants tels que MessageSid ou CallSid.

Une checklist de débogage en dix minutes

Passez des vérifications les moins coûteuses aux plus coûteuses :

  1. Bon secret, bon type ? Stripe CLI vs secret du Dashboard, Twilio Auth Token (la clé de signature) contre une API key, un secret rotation qui n’est pas encore propagé. GitHub omet entièrement X-Hub-Signature-256 s’il n’y a pas de secret configuré.
  2. Bon en-tête ? GitHub recommande X-Hub-Signature-256. Le plus ancien X-Hub-Signature utilise SHA-1 et est conservé pour des raisons de compatibilité. Les événements de test Stripe peuvent porter une entrée héritée v0 dans l’en-tête, alors vérifiez v1 et ignorez les autres schémas.
  3. Octets bruts ? Enregistrez la longueur du corps que vous hachez et comparez-la à Content-Length observée par le tunnel.
  4. Horloge dans la tolérance ? Imprimez now - timestamp à chaque rejet.
  5. Twilio : quelle URL hachez-vous ? Enregistrez-la et comparez caractère par caractère avec la Console. Essayez avec et sans le port et avec ou sans barre oblique finale.
  6. Est-ce que cela passe lorsque l’envoi est direct à l’application ? Si oui, quelque chose entre le fournisseur et votre application modifie la requête. Sinon, le bug est dans votre code de vérification.
  7. Comparaison en temps constant ? Utilisez crypto.timingSafeEqual ou hmac.compare_digest. Cela ne corrige pas une divergence, mais les docs GitHub le recommandent, et cela évite une fuite temporelle pendant le débogage.

Outils utiles à connaître à la fin de 2026

  • Stripe CLI. stripe listen transmet les événements vers localhost et affiche son secret de signature temporaire, donc aucune URL publique n’est nécessaire. Utilisez ce secret localement.
  • ngrok. Son Traffic Inspector peut afficher et rejouer des requêtes capturées, et sa fonction de vérification des fournisseurs peut vérifier les signatures webhook pour les fournisseurs pris en charge (Stripe et Mux, entre autres) et bloquer les requêtes invalides avant qu’elles n’atteignent votre app. Le guide Mux de ngrok indique une limite de 500 validations par mois sur les comptes gratuits.
  • @teamshift/webhook-inspect. Un paquet npm open-source récent qui vérifie Stripe, GitHub, Shopify, Slack, Twilio et des signatures HMAC génériques et explique pourquoi une vérification échoue. Son README décrit trois commandes:
    • verify vérifie une seule requête.
    • listen capture localement les webhooks, avec vérification.
    • replay envoie une capture enregistrée vers un serveur local. Lorsqu’une vérification échoue, l’outil tente des corrections concrètes et indique la cause confirmée.

Une mise en garde sur webhook-inspect : la liste Socket montre une seule version (0.1.0) et un seul mainteneur. Son README inclut une capture échantillon datée du 4 octobre 2026, mais je ne peux pas confirmer la date exacte de publication. Considérez-le comme un logiciel en phase précoce et examinez-le avant de faire transiter de vrais secrets via lui.

Quoi que vous utilisiez comme outil, la technique est identique : reproduisez la chaîne signée du fournisseur et modifiez une variable à la fois jusqu’à ce que les HMAC concordent.

Renforcement en production

  • Vérifiez avant de parser. Authentifiez d’abord, désérialisez ensuite.
  • Séparez les erreurs internes pour en-tête manquant, horodatage périmé et signature invalide, tout en donnant à l’appelant une réponse générique.
  • Rotation des secrets prise en charge. Stripe peut envoyer plusieurs signatures v1 tant que deux secrets sont actifs, et Standard Webhooks autorise plusieurs signatures séparées par des espaces, alors acceptez toute correspondance valide.
  • Dédupliquez sur X-GitHub-Delivery, l’ID d’événement Stripe, webhook-id ou les SIDs Twilio, car les retries sont normaux.
  • Utilisez une URL publique stable en développement. Pour la signature d’URL au style Twilio, un nom d’hôte de tunnel qui ne change pas entre les sessions maintient votre console fournisseur et votre validateur en synchronisation.
  • Ne publiez jamais un drapeau « skip verification » activé.

Questions fréquemment posées

Pourquoi ma signature vérifie localement mais échoue derrière un tunnel ?

Habituellement, les octets du corps (un parseur ou un proxy les a modifiés), l’horloge (dérive ou un événement réutilisé), ou l’URL (schéma, hôte ou chemin différents de celui signé). Écartez aussi le mauvais secret, surtout Stripe CLI versus Dashboard. Vérifiez les octets bruts, le décalage et l’URL reconstruit, dans cet ordre.

La signature de Twilio inclut-elle un horodatage ?

Non. Twilio signe l’URL complète plus les paramètres POST triés (ou se repose sur bodySHA256 pour les corps JSON). Stripe, Slack et Standard Webhooks incluent des horodatages.

Quelle est la bonne tolération d’horodatage ?

Cinq minutes (300 secondes) est la valeur par défaut largement utilisée dans les bibliothèques de Stripe et Standard Webhooks, et c’est aussi l’intervalle documenté par Slack. Élargissez-la uniquement lors des tests locaux.

Puis-je vérifier une signature après avoir analysé le JSON ?

Pas de manière fiable. Le JSON ré-serialisé peut différer en espaces, ordre des clés, échappements et formatage des nombres. Capturez les octets bruts avant l’exécution du parseur.

Pourquoi la validation Twilio échoue-t-elle lorsque votre hôte semble correct ?

Vérifiez le schéma, le port, le chemin, la chaîne de requête et la barre oblique finale. Un seul caractère peut modifier le HMAC. En cas de doute, codez l’URL en dur à partir de la Twilio Console.

Conclusion

Une signature webhook est un petit contrat : ces octets exacts, à ce moment précis, à cette adresse. Les tunnels et les proxys ne sont pas malveillants, mais ils se placent au milieu de ce contrat et peuvent perturber n’importe quelle clause. Conservez le corps brut, gardez votre horloge exacte, reconstruisez ou codez en dur l’URL exacte, et utilisez un outil de replay-diagnostic lorsque le décalage ne se résout pas en scrutant les journaux. Faites cela et « signé, scellé, livré » redevient « signé, scellé, livré ».


Sources

Continue from this article into the most relevant product guides and workflows.

Related Topics

#webhook signature verification#webhook signature failed#webhook signature mismatch#webhook signature debugging#webhook signature behind tunnel#webhook tunnel#localhost tunnel webhooks#ngrok webhook signature#ngrok webhook signature invalid#cloudflare tunnel webhooks#localtunnel webhooks#tunneling tools#webhook testing localhost#local webhook development#webhook debugging#webhook inspector#webhook security#HMAC signature verification#HMAC SHA256 webhook#raw body webhook verification#raw body vs parsed JSON#JSON re-serialization signature#express raw body middleware#body parser signature verification#webhook timestamp tolerance#webhook replay attack prevention#replay protection#clock skew webhook#Twilio signature validation#Twilio request validator#Twilio X-Twilio-Signature#Twilio webhook URL signing#Twilio webhook invalid signature#reverse proxy webhook#proxy rewrites host header#X-Forwarded-Host webhook#X-Forwarded-Proto signature#Host header rewrite#webhook URL mismatch#HTTPS termination webhook#Stripe webhook signature#Stripe webhook raw body#GitHub webhook signature#Shopify webhook HMAC#Slack signing secret#Slack request signature#webhook secret key#constant-time comparison#timing attack webhook#signature verification error#401 invalid signature webhook#webhook best practices#webhook troubleshooting#developer infrastructure#DevOps networking#API security#cloud-native development#HTTP request signing#webhook middleware#October 2026 webhook tools

Keep building with InstaTunnel

Read the docs for implementation details or compare plans before you ship.

Share this article

More InstaTunnel Insights

Discover more tutorials, tips, and updates to help you build better with localhost tunneling.

Browse All Articles