Le ticket disait simplement : « les webhooks de commande sont rejetés côté ERP depuis ce matin, signature invalide ». Rien n’avait pourtant changé côté configuration WooCommerce, ni le secret partagé, ni l’URL cible. Le genre de symptôme qui pousse instinctivement à soupçonner un problème réseau ou une horloge désynchronisée, alors que la cause était ailleurs.
Ce cas est intéressant précisément parce qu’il illustre un piège classique de la vérification HMAC : la signature ne protège pas un webhook contre une modification malveillante, elle protège contre toute modification, y compris celles que l’on introduit soi-même sans s’en rendre compte entre la réception et la vérification.
Symptôme : un rejet systématique, mais pas total
Premier élément troublant relevé dans les journaux du système tiers : environ 40 % des webhooks passaient sans problème, les autres étaient rejetés pour signature invalide. Pas de corrélation évidente avec l’heure d’envoi ni avec le type d’événement. En revanche, tous les webhooks rejetés partageaient un point commun une fois les commandes concernées ouvertes une à une : leur commentaire de commande ou leur adresse de livraison contenait un caractère accentué ou une apostrophe typographique.
Ce détail a immédiatement réorienté l’hypothèse vers un problème d’encodage plutôt que vers le secret partagé, d’autant que ce dernier n’avait pas été modifié récemment et fonctionnait pour les webhooks « simples ».
Diagnostic : où WooCommerce calcule réellement la signature
WooCommerce calcule la signature HMAC-SHA256 du corps JSON dans WC_Webhook::generate_signature(), à partir de la chaîne de caractères déjà sérialisée en JSON via wp_json_encode(). Le point critique : cette signature porte sur les octets exacts du JSON tel qu’envoyé, pas sur une représentation abstraite des données. Toute différence d’encodage entre la génération et la vérification, même invisible à l’œil nu, invalide la comparaison.
$payload = wp_json_encode( $webhook_payload );
$secret = $webhook->get_secret();
$signature = base64_encode( hash_hmac( 'sha256', $payload, $secret, true ) );

La cause réelle : un proxy applicatif qui reformatait le JSON
L’infrastructure du client faisait transiter les webhooks entrants par un petit proxy de journalisation avant l’ERP, chargé de logguer chaque requête pour audit. Ce proxy, écrit en Node.js, parsait le corps JSON reçu puis le re-sérialisait avant de le transmettre à l’ERP — une pratique courante pour normaliser un format, mais désastreuse pour une vérification de signature.
Le re-encodage JSON de Node.js échappait certains caractères Unicode différemment de wp_json_encode() côté PHP : un guillemet typographique ou une apostrophe issue d’une adresse de livraison n’était pas représenté par la même séquence d’octets après ce passage. La signature, calculée par WooCommerce sur le corps original, ne correspondait donc plus au corps effectivement reçu par l’ERP après reformattage, alors même que les données, elles, restaient identiques d’un point de vue métier.
Le correctif : vérifier avant toute transformation
La solution ne portait pas sur WooCommerce, entièrement conforme, mais sur le proxy : la vérification de signature devait s’exécuter sur le corps brut de la requête, avant tout parsing JSON, en conservant le buffer d’origine plutôt que de retravailler l’objet désérialisé.
- Capturer le corps brut de la requête dans le proxy, sans le parser d’abord
- Calculer la signature HMAC sur ce corps brut exact
- Comparer avec l’en-tête
X-WC-Webhook-Signatureavant toute autre étape - Ne parser le JSON qu’une fois la vérification validée
Prévention pour les prochaines intégrations
Le principe général à retenir dépasse ce seul cas : dans toute chaîne comportant un proxy, un WAF applicatif ou un middleware de journalisation, la vérification de signature doit toujours intervenir sur les octets les plus proches possible de la réception réseau, avant toute réécriture, même anodine en apparence.
Un corps JSON « équivalent » n’est pas un corps JSON identique. La cryptographie ne raisonne jamais sur le sens des données, seulement sur leurs octets exacts.
Ce qu’il faut vérifier en premier la prochaine fois
- Le taux d’échec est-il total ou partiel ? Un taux partiel oriente vers un contenu spécifique, pas vers le secret
- Existe-t-il un composant intermédiaire entre WooCommerce et le point de vérification ?
- Ce composant reformate-t-il, journalise-t-il ou transforme-t-il le corps de la requête ?
En résumé
Un rejet de signature HMAC n’est pas toujours un problème de secret partagé mal copié : dès qu’un composant intermédiaire retouche le corps de la requête, même sans en changer le sens, la vérification échoue. Face à un taux d’échec partiel et corrélé à un type de contenu précis, chercher du côté de l’encodage avant de suspecter la configuration est souvent le chemin le plus rapide vers la cause réelle.