vendredi 25 septembre 2026

À propos

Contact

Sécurité

Audit d’une intégration Stripe mal sécurisée : webhook non vérifié, clé exposée

Étude de cas d'une boutique WooCommerce dont l'intégration Stripe maison exposait la clé secrète côté client et ne vérifiait pas la signature du webhook. Correctifs appliqués.

Par Clément Hadrot • 14 avril 2022 • 6 min de lecture • Aucun commentaire
Audit d'une intégration Stripe mal sécurisée : webhook non vérifié, clé exposée

Un client exploitant une boutique WooCommerce de vente de formations en ligne nous sollicite après un comportement étrange : des commandes apparaissent comme payées dans WordPress alors qu’aucun paiement correspondant n’existe côté Stripe. L’intégration en cause n’est pas l’extension officielle WooCommerce Stripe, mais un développement maison réalisé par un précédent prestataire pour gérer un cas particulier de facturation récurrente non couvert par l’extension standard.

L’audit du code révèle deux problèmes indépendants, chacun suffisant à lui seul pour expliquer une partie des anomalies constatées, et dont la combinaison rendait la situation particulièrement sérieuse.

Premier problème : la clé secrète exposée côté client

Stripe distingue deux types de clés API : une clé publique (préfixée pk_), conçue pour être intégrée dans le JavaScript exécuté dans le navigateur du visiteur, et une clé secrète (préfixée sk_), qui ne doit jamais quitter le serveur. La première permet uniquement de créer des jetons de paiement côté client ; la seconde permet d’effectuer n’importe quelle opération sur le compte Stripe, y compris consulter l’historique des paiements ou émettre des remboursements.

En inspectant le code source de la page de paiement, la clé secrète complète, préfixée sk_live_, apparaissait en clair dans un bloc <script> inline, visible par quiconque ouvrait les outils de développement du navigateur :

<script>
// Code fautif retrouvé dans le thème enfant
var stripe = Stripe('sk_live_51H8xxxxxxxxxxxxxxxxxxxxxx');
// ...
</script>

La fonction Stripe() côté JavaScript attend une clé publique ; utiliser la clé secrète à cet endroit fonctionne techniquement pour créer un paiement, mais expose cette clé à absolument tous les visiteurs du site, y compris ceux qui n’ont jamais eu l’intention de payer. La première action a été la révocation immédiate de cette clé depuis le tableau de bord Stripe et la génération d’une nouvelle paire clé publique/clé secrète.

Second problème : un webhook qui acceptait n’importe quelle requête

L'essentiel à retenir : Une clé secrète Stripe n'a rien à faire côté client ; Un webhook non vérifié accepte n'importe quelle requête ; La signature Stripe se vérifie avec une fonction dédiée

Le second mécanisme en cause explique directement les commandes marquées payées sans paiement réel : un point d’entrée REST personnalisé, /wp-json/moncpt/v1/webhook-stripe, recevait les notifications Stripe (confirmation de paiement, échec, remboursement) et mettait à jour le statut de la commande WooCommerce en conséquence — mais sans jamais vérifier que la requête provenait réellement de Stripe.

// Code fautif : aucune vérification de la provenance de la requête
add_action( 'rest_api_init', function() {
    register_rest_route( 'moncpt/v1', '/webhook-stripe', array(
        'methods'  => 'POST',
        'callback' => 'moncpt_traiter_webhook_stripe',
        'permission_callback' => '__return_true',
    ) );
} );

function moncpt_traiter_webhook_stripe( $request ) {
    $donnees = $request->get_json_params();

    if ( 'payment_intent.succeeded' === $donnees['type'] ) {
        $commande = wc_get_order( $donnees['data']['object']['metadata']['order_id'] );
        $commande->update_status( 'completed' );
    }

    return new WP_REST_Response( array( 'received' => true ), 200 );
}

N’importe qui, ayant compris le format attendu par simple observation ou déduction, pouvait envoyer une requête POST directement à cette route avec un identifiant de commande de son choix et un type d’événement payment_intent.succeeded fabriqué de toutes pièces, sans avoir payé quoi que ce soit. C’est exactement ce qui s’est produit : quelqu’un avait deviné ou testé ce comportement et validait des commandes gratuitement.

Le correctif : vérifier la signature Stripe

Stripe signe chaque requête de webhook avec un en-tête Stripe-Signature, calculé à partir d’un secret de webhook distinct des clés API, fourni lors de la configuration du point d’entrée dans le tableau de bord Stripe. La bibliothèque officielle PHP de Stripe expose une fonction dédiée à cette vérification, qui doit être la toute première étape du traitement, avant même de lire le contenu de la requête :

function moncpt_traiter_webhook_stripe( $request ) {
    $payload          = $request->get_body();
    $signature_entete = $request->get_header( 'stripe_signature' );
    $secret_webhook   = getenv( 'STRIPE_WEBHOOK_SECRET' );

    try {
        $evenement = \Stripe\Webhook::constructEvent(
            $payload,
            $signature_entete,
            $secret_webhook
        );
    } catch ( \Stripe\Exception\SignatureVerificationException $e ) {
        // Signature absente ou invalide : requête rejetée sans traitement.
        return new WP_REST_Response( array( 'error' => 'signature invalide' ), 400 );
    }

    if ( 'payment_intent.succeeded' === $evenement->type ) {
        $commande_id = $evenement->data->object->metadata->order_id ?? null;
        $commande    = $commande_id ? wc_get_order( $commande_id ) : false;

        if ( $commande ) {
            $commande->update_status( 'completed' );
        }
    }

    return new WP_REST_Response( array( 'received' => true ), 200 );
}

constructEvent() recalcule la signature attendue à partir du corps brut de la requête et du secret de webhook, puis la compare à celle fournie dans l’en-tête. Toute divergence — requête forgée, corps modifié en transit, secret incorrect — déclenche une exception et bloque le traitement avant qu’aucune commande ne soit modifiée.

Ce que ce cas révèle sur les intégrations de paiement maison

Les deux failles partagent une origine commune : un développement réalisé rapidement pour répondre à un besoin métier précis, sans revue de sécurité dédiée aux paiements, un domaine où l’extension officielle WooCommerce Stripe applique par défaut l’ensemble de ces vérifications, précisément parce qu’elle a été auditée et éprouvée à grande échelle. Le développement maison reste parfois nécessaire pour des cas non couverts, mais il doit alors reproduire ce niveau de rigueur, pas s’en affranchir.

Sur cet audit, la vérification de signature de webhook a été la correction la plus urgente : sans elle, la faille restait exploitable même après la révocation de la clé secrète exposée, puisque le point d’entrée du webhook n’avait jamais eu besoin de cette clé pour être abusé.

En résumé

Cette étude de cas ne couvre pas la sécurisation générale des webhooks entrants au-delà du contexte Stripe (autres fournisseurs de paiement, autres services tiers), un sujet traité séparément avec ses propres mécanismes de signature. Le principe reste transposable : toute notification reçue d’un service tiers doit être authentifiée avant d’être traitée, jamais acceptée sur la seule foi de son format apparent.

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