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

Sécurité

Un module de dons Stripe : la faille qui laissait modifier le montant

Un formulaire de don personnalisé transmettait le montant choisi par le visiteur sans le revalider côté serveur. Analyse défensive d'une faille de confiance côté client.

Par Clément Hadrot • 18 avril 2021 • 4 min de lecture • Aucun commentaire
Un module de dons Stripe : la faille qui laissait modifier le montant

Un centime au lieu de cent euros : c’est le montant réellement débité lors d’un test d’intrusion mené sur un module de don personnalisé, alors que l’interface affichait fièrement le montant choisi par le généreux testeur. La différence entre les deux ne tenait qu’à une requête modifiée avant son envoi à l’API Stripe.

Ce module avait été développé sur mesure pour une association, avec un formulaire proposant des montants prédéfinis et un champ libre pour saisir une somme personnalisée. Le fonctionnement semblait irréprochable côté visiteur : impossible de saisir un montant négatif ou nul dans le champ, validation JavaScript immédiate, message d’erreur clair. Le problème ne se situait pas dans cette couche visible, mais dans celle qui suivait.

Où se cachait la confiance mal placée

Le formulaire envoyait le montant choisi par le visiteur dans le corps de la requête vers un point de terminaison REST personnalisé, qui créait ensuite le PaymentIntent Stripe correspondant côté serveur. Jusque-là, rien d’anormal : c’est le schéma d’intégration recommandé, où la clé secrète Stripe reste côté serveur et où seul un jeton client (client_secret) transite vers le navigateur pour finaliser le paiement.

Le problème tenait dans un seul appel PHP : le montant transmis par le visiteur était utilisé tel quel pour créer le PaymentIntent, sans jamais être recomparé à une liste de montants autorisés ni recalculé côté serveur. Rien n’empêchait donc de modifier ce montant avant l’envoi, via les outils de développement du navigateur ou un simple client HTTP, tout en laissant l’interface afficher la somme initialement choisie.

Pourquoi cette confiance semble raisonnable au premier abord

Le validation côté client donne une fausse impression de sécurité : elle empêche effectivement un visiteur ordinaire de saisir un montant absurde par erreur, ce qui couvre l’écrasante majorité des usages réels du formulaire. Mais toute validation exécutée dans le navigateur reste, par nature, sous le contrôle total de la personne qui utilise ce navigateur. Un champ désactivé, une contrainte min ou max en HTML, une vérification JavaScript avant soumission : aucun de ces mécanismes ne protège contre une requête forgée directement, en dehors de l’interface prévue.

L'essentiel à retenir : Un montant transmis par le navigateur n'est jamais fiable ; PaymentIntent doit être créé avec un montant recalculé côté serveur ; Un simple test avec les outils de développement suffit à détecter la faille

Le correctif : ne jamais faire confiance à un montant reçu

La correction a consisté à séparer strictement deux rôles : le navigateur choisit une intention de montant, le serveur seul décide du montant réellement facturé. Concrètement, la route REST recalcule ou valide systématiquement le montant reçu avant toute création de PaymentIntent :

register_rest_route('dons/v1', '/creer-intention', [
    'methods'  => 'POST',
    'callback' => function (WP_REST_Request $request) {
        $montant_centimes = (int) $request->get_param('montant');

        if ($montant_centimes < 500 || $montant_centimes > 500000) {
            return new WP_Error('montant_invalide', 'Montant hors limites autorisées.', ['status' => 400]);
        }

        $intent = \Stripe\PaymentIntent::create([
            'amount'   => $montant_centimes,
            'currency' => 'eur',
            'metadata' => ['origine' => 'formulaire_don_site'],
        ]);

        return ['client_secret' => $intent->client_secret];
    },
    'permission_callback' => '__return_true',
]);

Les bornes minimale et maximale, définies côté serveur, correspondent aux limites métier réellement souhaitées par l’association, indépendamment de ce que l’interface propose visuellement.

Vérifier qu’un formulaire de paiement résiste à ce test

Le test qui a révélé cette faille ne demandait aucun outil sophistiqué : ouvrir les outils de développement du navigateur, repérer la requête envoyée lors de la soumission du formulaire, la rejouer avec un montant modifié via l’onglet réseau, puis observer si le paiement final correspond au montant modifié ou au montant initialement affiché. Ce test simple devrait faire partie de toute recette de module de paiement personnalisé, avant même de considérer la conformité PCI ou la gestion des remboursements.

Notre verdict

Un montant choisi par un visiteur reste une intention, jamais une donnée de confiance. La règle qui en découle est générale et dépasse largement le cas de Stripe : tout champ modifiable par un utilisateur, dès lors qu’il influence une action ayant une conséquence financière ou irréversible, doit être revalidé intégralement côté serveur, sans exception liée à la présence d’une validation côté client déjà en place.

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