Comparé à une simple validation JavaScript côté client, un hook PHP accroché sur woocommerce_checkout_process paraît robuste : il s’exécute côté serveur, il peut bloquer la commande, il semble donc à l’abri des manipulations. C’est exactement ce raisonnement qui a conduit un audit de code, mené sur une boutique vendant des produits sur devis avec un champ « numéro de bon de commande » obligatoire, à conclure que la validation était solide. Elle ne l’était pas.
Le code fonctionnait parfaitement dans le navigateur, avec le formulaire de paiement standard. Le problème est apparu en testant un scénario que personne n’avait envisagé : une requête POST construite directement vers /?wc-ajax=checkout, en dehors de toute page de paiement, sans jamais charger le formulaire ni son jeton de sécurité.
Ce qu’on voit
Le code incriminé ressemblait à ceci, extrait presque à l’identique du dépôt audité :
add_action( 'woocommerce_checkout_process', function () {
if ( empty( $_POST['numero_bon_commande'] ) ) {
wc_add_notice( 'Le numéro de bon de commande est obligatoire.', 'error' );
}
} );
À première vue, rien ne cloche : le champ est bien contrôlé, un message d’erreur bloque la validation si le champ est vide. Le hook woocommerce_checkout_process est d’ailleurs le hook officiel documenté pour ce type de vérification, ce qui renforçait la confiance de l’équipe dans sa solidité.
Pourquoi c’est un problème
Le hook woocommerce_checkout_process se déclenche dès que WooCommerce traite une soumission de paiement, quelle que soit son origine — y compris une requête forgée qui ne provient jamais du formulaire HTML réel. Rien dans ce code ne vérifie qu’un jeton de sécurité valide (un nonce WordPress) accompagne la requête. Or WooCommerce génère bien un nonce sur la page de paiement (woocommerce-process-checkout-nonce), mais ne le vérifie pas automatiquement pour les champs personnalisés ajoutés par un développeur.

Concrètement, un script externe peut poster directement un champ numero_bon_commande rempli avec n’importe quelle valeur (y compris une valeur copiée d’une autre commande légitime), sans jamais passer par les contrôles métier plus larges qu’un formulaire réel aurait pu imposer côté JavaScript. Le contournement ne demande ni compte utilisateur ni accès privilégié : une simple requête HTTP suffit.
La confusion entre « hook métier » et « origine vérifiée »
L’erreur de fond est de confondre deux notions distinctes : le fait qu’un hook s’exécute côté serveur (ce qui est vrai et rassurant) et le fait que la requête qui le déclenche provient bien du parcours attendu (ce qui ne l’est pas du tout, sans vérification explicite). WooCommerce fournit les outils pour cette seconde vérification, mais ne l’impose pas automatiquement sur les hooks de validation personnalisés.
Quoi faire
Le correctif consiste à vérifier explicitement le nonce généré par WooCommerce avant de traiter la validation :
add_action( 'woocommerce_checkout_process', function () {
if ( ! isset( $_POST['woocommerce-process-checkout-nonce'] )
|| ! wp_verify_nonce( $_POST['woocommerce-process-checkout-nonce'], 'woocommerce-process_checkout' ) ) {
wc_add_notice( 'Session expirée, veuillez recharger la page.', 'error' );
return;
}
if ( empty( $_POST['numero_bon_commande'] ) ) {
wc_add_notice( 'Le numéro de bon de commande est obligatoire.', 'error' );
}
} );
Cette vérification ne rend pas la commande impossible à forger par un attaquant déterminé — un nonce peut être récupéré en chargeant la page légitimement avant l’envoi — mais elle élimine les scripts automatisés génériques et impose au minimum un passage réel par le parcours prévu, ce qui suffit dans l’immense majorité des cas rencontrés.
- Toujours vérifier le nonce
woocommerce-process-checkout-noncedans un hook de validation personnalisé. - Ne jamais faire confiance à un champ
$_POSTsans échappement ni contrôle de format. - Documenter, dans le code, pourquoi chaque champ personnalisé est considéré comme fiable ou non.
Bilan de l’audit
Ce n’est pas un cas isolé : sur plusieurs bases de code auditées, la même confusion revient dès qu’un développeur ajoute un champ de paiement personnalisé sans relire la documentation sur la sécurisation des formulaires. Ce constat ne s’étend volontairement pas à la sécurisation des points d’entrée de l’API REST, qui répond à une logique de jetons différente et mérite un traitement à part.