Le WordPress d'aujourd'hui, décodé pour les développeurs

Sécurité

Sécuriser un webhook Brevo entrant : signature HMAC et rejeu

X-Sib-Signature dans les en-têtes, un webhook Brevo qui annonce un désabonnement : encore faut-il vérifier que ce message vient réellement de Brevo avant de l'écrire en base.

Par Clément Hadrot • 24 mai 2024 • 5 min de lecture • Aucun commentaire
Sécuriser un webhook Brevo entrant : signature HMAC et rejeu

Un webhook Brevo qui annonce un désabonnement, reçu par un site WordPress qui synchronise ses statuts de délivrabilité avec sa base de contacts locale, ne devrait jamais être traité sans vérification préalable de son origine réelle. Si l’endpoint qui reçoit ce webhook est connu ou devinable, rien n’empêche un tiers d’envoyer une requête qui imite parfaitement le format attendu, sans jamais être passée par les serveurs de Brevo. La conséquence la plus problématique : un statut de désabonnement falsifié pourrait remettre en circulation, ou au contraire retirer abusivement, un contact réel de la base locale.

Brevo, comme la plupart des plateformes d’emailing, signe ses webhooks sortants avec une clé secrète partagée, transmise dans un en-tête HTTP dédié. Cette signature permet au serveur qui reçoit le webhook de vérifier, avant tout traitement, que la requête provient bien de Brevo et n’a pas été modifiée en chemin. Voici le snippet de vérification à écrire avant toute écriture en base WordPress.

Vérifier la signature avant tout traitement

function traiter_webhook_brevo( WP_REST_Request $request ) {
    $corps_brut = $request->get_body();
    $signature_recue = $request->get_header( 'x-sib-signature' );

    $signature_calculee = hash_hmac( 'sha256', $corps_brut, BREVO_WEBHOOK_SECRET );

    if ( ! hash_equals( $signature_calculee, $signature_recue ) ) {
        return new WP_Error( 'signature_invalide', 'Signature webhook rejetée.', array( 'status' => 401 ) );
    }

    $evenement = json_decode( $corps_brut, true );
    // traitement de l'événement seulement à partir d'ici
}

Le point essentiel de ce snippet est l’usage de hash_equals() plutôt que d’une comparaison directe avec === ou strcmp(). Une comparaison naïve de deux chaînes de caractères peut, sur certaines implémentations, révéler par le temps de réponse la position du premier caractère différent, ce qui permettrait en théorie de reconstituer la signature attendue octet par octet. hash_equals() effectue une comparaison à temps constant, conçue précisément pour ce type de vérification cryptographique.

Empêcher le rejeu d’un webhook intercepté

L'essentiel à retenir : Une signature HMAC vérifie l'origine du message, pas seulement son format ; Un webhook rejoué doit être détecté et rejeté indépendamment de la signature ; Les statuts de désabonnement doivent primer sur tout traitement contradictoire ultérieur

La vérification de signature garantit l’origine du message, mais pas son unicité dans le temps : un webhook intercepté et signé légitimement pourrait, en théorie, être renvoyé plusieurs fois vers l’endpoint pour rejouer le même événement. La protection contre ce rejeu repose sur l’identifiant d’événement fourni par Brevo dans le corps du webhook :

function webhook_deja_traite( $evenement_id ) {
    global $wpdb;
    $table = $wpdb->prefix . 'brevo_webhooks_traites';

    $existe = $wpdb->get_var( $wpdb->prepare(
        "SELECT id FROM {$table} WHERE evenement_id = %s",
        $evenement_id
    ) );

    if ( $existe ) {
        return true;
    }

    $wpdb->insert( $table, array(
        'evenement_id' => $evenement_id,
        'traite_le'    => current_time( 'mysql' ),
    ) );

    return false;
}

Cette table dédiée, purgée périodiquement des entrées de plus de quelques semaines, conserve la trace de chaque événement déjà traité. Un webhook rejoué, même parfaitement signé, est ainsi ignoré silencieusement plutôt que retraité une seconde fois.

Le cas particulier des désabonnements

Les statuts de désabonnement méritent un traitement encore plus prudent, car leur enjeu dépasse la simple cohérence de données : un désabonnement raté expose à l’envoi de communications à une personne qui les a explicitement refusées. La logique de traitement du webhook a été conçue pour qu’un statut de désabonnement, une fois enregistré, ne puisse plus être écrasé par un événement contradictoire ultérieur sans passage par une validation manuelle, contrairement aux autres statuts de délivrabilité (ouverture, clic) qui restent purement informatifs.

Où stocker le secret partagé

La constante BREVO_WEBHOOK_SECRET utilisée dans le snippet de vérification doit être définie dans wp-config.php, jamais dans une option de base de données ni dans le code source versionné de l’extension. Ce secret, généré côté interface Brevo lors de la configuration du webhook, doit rester strictement identique des deux côtés pour que la vérification de signature fonctionne, et régénéré si un doute existe sur sa confidentialité.

  • Vérifier la signature HMAC avant tout parsing du corps de la requête.
  • Rejeter avec un code 401 générique, sans détail sur la raison précise du rejet.
  • Suivre les identifiants d’événements déjà traités pour empêcher tout rejeu.
  • Traiter les désabonnements avec une priorité supérieure aux autres statuts.

Un webhook qui semble légitime au format ne l’est réellement que si sa signature le confirme : la forme d’un message ne garantit jamais son origine.

Ce qu’on retient

La vérification de signature HMAC sur un webhook entrant est un réflexe qui devrait s’appliquer à toute intégration de ce type, quel que soit le fournisseur. Sur ce projet, elle a permis d’éviter qu’un endpoint de synchronisation, initialement pensé comme un simple point d’entrée technique sans enjeu particulier, ne devienne un vecteur de falsification des statuts de consentement d’une base de contacts.

Partager :

À propos de l'auteur

Clément Hadrot

Développeur WordPress, passionné par Elementor, le FSE et l’automatisation par IA.

Voir tous ses articles

Dans la même veine

À lire aussi