vendredi 25 septembre 2026

À propos

Contact

Sécurité

Un abonnement WooCommerce détourné par une faille de logique métier

Un client a retrouvé son abonnement WooCommerce actif sans paiement associé. L'enquête a mené à une faille de logique métier, invisible pour un scanner classique.

Par Clément Hadrot • 10 novembre 2023 • 6 min de lecture • Aucun commentaire
Un abonnement WooCommerce détourné par une faille de logique métier

Le ticket est arrivé un lundi matin, classé « facturation », avec une phrase qui ne collait pas : un client indiquait qu’un de ses abonnés bénéficiait d’un accès premium depuis trois semaines sans qu’aucun prélèvement Stripe ne soit visible dans le tableau de bord. Rien dans les journaux de paiement ne laissait supposer une fraude par carte volée, le motif habituel. L’abonnement WooCommerce affichait simplement le statut active, comme n’importe quel abonné en règle.

Ce genre de signalement finit souvent classé sans suite : erreur de synchronisation, webhook Stripe en retard, ou simple confusion du client. Mais l’absence totale de trace de paiement, même échoué, méritait qu’on creuse. Ce qui a suivi a mis au jour une faille qui n’avait rien à voir avec une injection ou une faille XSS classique : une faille de logique métier, dans l’enchaînement des étapes du renouvellement d’abonnement.

Un statut qui change sans jamais passer par la caisse

WooCommerce Subscriptions gère le cycle de vie d’un abonnement à travers une série de statuts : pending, active, on-hold, cancelled, expired. Le passage d’un statut à l’autre est censé être conditionné par des événements précis : validation d’une commande, confirmation d’un paiement récurrent, échec de prélèvement suivi d’une relance. Sur ce site, une extension maison avait été développée pour permettre à un client de « mettre en pause puis reprendre » son abonnement depuis son espace personnel, une fonctionnalité que WooCommerce ne propose pas nativement de façon aussi simple.

L’extension exposait une route REST personnalisée, quelque chose comme /wp-json/monsite/v1/subscription/resume, qui prenait l’identifiant de l’abonnement et repassait son statut à active. Le développeur avait bien vérifié que l’utilisateur connecté était propriétaire de l’abonnement avec current_user_can et une comparaison d’identifiant. Ce qu’il n’avait pas prévu, c’est que cette route pouvait être appelée directement, sans jamais passer par le flux normal de mise en pause préalable, ni par la vérification qu’un moyen de paiement valide était toujours enregistré.

Reconstituer la chronologie des appels

L'essentiel à retenir : Rejeu de requêtes dans le mauvais ordre ; Un statut changé sans vérification de paiement ; Les scanners ne testent pas les séquences d'actions

Retrouver l’origine du problème a demandé de reconstituer, requête par requête, ce que l’abonné avait fait. Les journaux d’accès du serveur web ont montré la séquence exacte : une requête POST vers la route de reprise, envoyée directement, sans qu’aucune requête de mise en pause n’ait eu lieu avant. L’abonné avait simplement découvert l’endpoint en observant le trafic réseau de son propre navigateur pendant une utilisation normale de son compte, puis l’avait rejoué plus tard avec un outil comme curl.

La route ne vérifiait à aucun moment l’état précédent de l’abonnement avant d’accepter la transition. Un abonnement cancelled depuis des mois pouvait ainsi redevenir active d’un simple appel, sans jamais déclencher la moindre tentative de prélèvement. Le code, isolément, semblait raisonnable : il vérifiait les permissions, échappait les entrées, utilisait wpdb::prepare pour ses requêtes. Rien qu’un audit de code classique, ligne par ligne, n’aurait signalé comme dangereux.

Ce que seul un test de parcours révèle

La différence entre une faille technique et une faille de logique métier tient précisément là : la première se voit dans le code, isolément. La seconde ne se révèle qu’en questionnant l’enchaînement des états. Un scanner de vulnérabilités, qu’il s’agisse de WPScan ou d’un outil d’analyse statique, cherche des motifs connus : une entrée non échappée, une fonction dangereuse, une dépendance vulnérable. Aucun de ces outils ne modélise le cycle de vie métier d’un abonnement pour vérifier qu’une transition d’état est cohérente avec ce qui a précédé.

Pour couvrir ce type de risque, il faut une revue spécifique, centrée sur les machines à états de l’application :

  • Lister chaque statut possible d’un objet métier (abonnement, commande, remboursement) et les transitions autorisées entre eux.
  • Pour chaque endpoint qui modifie un statut, vérifier explicitement le statut de départ, pas seulement les permissions de l’utilisateur.
  • Rejouer les parcours dans le désordre : sauter des étapes, revenir en arrière, appeler deux fois la même route.
  • Vérifier que les événements sensibles (facturation, accès) sont toujours déclenchés par un événement source de confiance, comme un webhook Stripe signé, plutôt que par une action utilisateur directe.

Le correctif appliqué

La solution a consisté à faire en sorte que la route de reprise ne change jamais directement le statut de l’abonnement. À la place, elle déclenche une tentative de prélèvement via l’API Stripe, et c’est uniquement le webhook invoice.payment_succeeded qui autorise WooCommerce Subscriptions à repasser l’abonnement en active, par son mécanisme natif de gestion des statuts. Le code source ressemble à ceci :

register_rest_route( 'monsite/v1', '/subscription/resume', array(
    'methods'  => 'POST',
    'callback' => 'monsite_resume_subscription',
    'permission_callback' => function( $request ) {
        $subscription = wcs_get_subscription( $request['id'] );
        return $subscription
            && get_current_user_id() === $subscription->get_user_id()
            && $subscription->has_status( 'on-hold' );
    },
) );

function monsite_resume_subscription( $request ) {
    $subscription = wcs_get_subscription( $request['id'] );
    // On ne touche jamais au statut ici : on déclenche uniquement
    // une tentative de paiement, le webhook fera la transition.
    $subscription->add_order_note( 'Reprise demandée, tentative de paiement lancée.' );
    return $subscription->get_last_order()->payment_complete_reminder_email();
}

La vérification du statut de départ dans permission_callback ferme la porte à un appel direct depuis n’importe quel état. Et surtout, plus aucune route ne peut faire passer un abonnement à active sans qu’un paiement réel n’ait été confirmé.

Sur ce genre d’audit, la question qui rapporte le plus n’est jamais « ce code est-il vulnérable ? » mais « que se passe-t-il si j’appelle cette route dans le désordre, ou deux fois de suite ? ».

Ce qu’on a changé après coup

Cet incident a changé la manière dont les revues de code sont menées sur les projets suivants de l’agence. Chaque route REST personnalisée touchant à la facturation fait désormais l’objet d’un schéma de transitions d’états, documenté avant même l’écriture du code, et d’un test automatisé qui tente explicitement les transitions interdites. Ce n’est pas un outil qu’on installe, c’est une discipline qu’on impose à la conception. Les scanners automatiques restent utiles pour les failles connues et répertoriées, mais ils ne remplaceront jamais la question simple posée à voix haute pendant une revue : « et si quelqu’un appelait ça dans le désordre ? »

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