vendredi 25 septembre 2026

À propos

Contact

Sécurité

CSRF dans les extensions WordPress : ce qui se joue au-delà des nonces

Un nonce seul ne suffit jamais à sécuriser une action AJAX. Voici comment vérifier l'origine des requêtes et combiner nonce et capacité correctement.

Par Clément Hadrot • 10 novembre 2021 • 6 min de lecture • Aucun commentaire
CSRF dans les extensions WordPress : ce qui se joue au-delà des nonces

Les nonces sont souvent présentés comme LA solution au CSRF dans WordPress, et c’est en partie vrai : ils empêchent qu’une requête forgée depuis un site tiers, à l’insu de l’utilisateur, soit acceptée comme légitime. Mais réduire la protection CSRF à la seule présence d’un nonce est une erreur fréquente, qui laisse des extensions vulnérables malgré une vérification de nonce en apparence correcte. Le CSRF, dans une extension WordPress bien conçue, est une question de défense en profondeur, pas d’une seule ligne de code.

Reprenons le problème depuis le début : le CSRF consiste à faire exécuter, à l’insu d’un utilisateur authentifié, une action qu’il n’a pas voulue, en s’appuyant sur le fait que son navigateur envoie automatiquement ses cookies de session à chaque requête vers le site. Le nonce casse ce mécanisme en exigeant une preuve supplémentaire, générée uniquement par WordPress pour cette session et cette action précises. Mais un nonce mal exploité, ou utilisé seul, laisse encore des angles morts.

Le piège du nonce sans vérification de capacité

Un nonce prouve qu’une requête provient d’une page générée par WordPress pour l’utilisateur courant. Il ne prouve absolument rien sur ce que cet utilisateur a le droit de faire. Voici un exemple de code vulnérable malgré la présence d’un nonce :

add_action( 'wp_ajax_mon_plugin_supprimer_element', 'mon_plugin_supprimer_element' );

function mon_plugin_supprimer_element() {
    check_ajax_referer( 'mon_plugin_action', 'nonce' );

    $id = absint( $_POST['id'] );
    mon_plugin_supprimer_en_base( $id );

    wp_send_json_success();
}

Ce code vérifie bien le nonce, mais n’importe quel utilisateur connecté, même un simple abonné sans aucun privilège d’administration, peut appeler cette action AJAX dès lors qu’il obtient un nonce valide pour son propre compte, généré simplement en visitant n’importe quelle page qui charge ce nonce en JavaScript. La correction ajoute la vérification de capacité qui manque :

function mon_plugin_supprimer_element() {
    check_ajax_referer( 'mon_plugin_action', 'nonce' );

    if ( ! current_user_can( 'manage_options' ) ) {
        wp_send_json_error( 'Action non autorisée.', 403 );
    }

    $id = absint( $_POST['id'] );
    mon_plugin_supprimer_en_base( $id );

    wp_send_json_success();
}

admin-ajax.php : un point d’entrée unique à protéger action par action

Toutes les requêtes AJAX de l’administration WordPress, quelle que soit l’extension à l’origine, transitent par le même fichier, admin-ajax.php. WordPress distingue les actions via les hooks wp_ajax_{action} pour les utilisateurs connectés et wp_ajax_nopriv_{action} pour les visiteurs non connectés. C’est ce second hook qui mérite une attention particulière : toute action enregistrée avec wp_ajax_nopriv_ est accessible à n’importe qui sur Internet, connecté ou non.

L'essentiel à retenir : Un nonce sans vérification de capacité laisse la porte ouverte à toute personne connectée ; admin-ajax.php centralise les actions AJAX et doit être protégé action par action ; Vérifier nonce et current_user_can ensemble, jamais l'un sans l'autre

Il est donc essentiel, à chaque nouvelle action AJAX ajoutée dans une extension, de se poser la question : cette action doit-elle vraiment être accessible sans authentification ? Si la réponse est non, seul le hook wp_ajax_ doit être utilisé, jamais son équivalent nopriv, qui ouvre l’action à un public bien plus large que nécessaire.

Vérifier l’origine au-delà du nonce

Pour des actions particulièrement sensibles, une vérification supplémentaire de l’en-tête Referer ou de l’origine de la requête peut compléter le dispositif, même si WordPress l’intègre déjà partiellement dans check_admin_referer(). Les fonctions check_admin_referer() et check_ajax_referer() vérifient en réalité à la fois le nonce et, dans une certaine mesure, la cohérence de la requête avec la session, ce qui les rend préférables à un simple wp_verify_nonce() isolé quand le contexte le permet.

  • Privilégier check_admin_referer() pour les formulaires classiques d’administration plutôt qu’un wp_verify_nonce() nu ;
  • Privilégier check_ajax_referer() pour les actions AJAX, qui renvoie directement une erreur JSON cohérente en cas d’échec ;
  • Ne jamais s’appuyer uniquement sur l’en-tête Referer comme seule protection, car il peut être absent ou falsifié selon la configuration du navigateur ou un proxy intermédiaire ; il vient en complément du nonce, jamais à sa place.

Le cas de l’API REST

Les points de terminaison de l’API REST WordPress suivent une logique différente des actions AJAX classiques : ils utilisent l’argument permission_callback lors de l’enregistrement de la route, qui doit impérativement être défini pour éviter qu’un point de terminaison sensible ne reste accessible sans aucune vérification :

register_rest_route( 'mon-plugin/v1', '/elements/(?P<id>\d+)', array(
    'methods'             => 'DELETE',
    'callback'            => 'mon_plugin_rest_supprimer_element',
    'permission_callback' => function() {
        return current_user_can( 'manage_options' );
    },
) );

Pour les requêtes REST authentifiées par cookie (typiquement depuis l’administration ou un thème connecté), WordPress exige par défaut un en-tête X-WP-Nonce généré via wp_create_nonce( 'wp_rest' ), vérifié automatiquement par le cœur de WordPress. Oublier de définir permission_callback, ou définir une fonction qui retourne systématiquement true, revient à supprimer toute protection CSRF et toute vérification de droits sur la route concernée.

La checklist à appliquer sur chaque action sensible

  1. Un nonce est-il généré côté formulaire ou script, avec une action spécifique et non générique ?
  2. Ce nonce est-il vérifié côté serveur avec check_admin_referer() ou check_ajax_referer() ?
  3. La capacité de l’utilisateur est-elle vérifiée séparément avec current_user_can(), avec la capacité la plus précise possible ?
  4. L’action AJAX utilise-t-elle wp_ajax_nopriv_ alors qu’elle ne le devrait pas ?
  5. La route REST associée définit-elle un permission_callback qui vérifie réellement quelque chose ?

Je recommande de traiter chaque nouvelle action sensible d’une extension comme une liste de contrôle à cocher entièrement avant de la considérer terminée : nonce généré, nonce vérifié, capacité vérifiée, hook AJAX correctement scopé. Sauter une seule case suffit à rouvrir la porte que les autres avaient fermée.

En résumé

Le nonce reste une pièce indispensable de la protection contre le CSRF dans WordPress, mais ce n’est qu’une pièce parmi d’autres. Une action réellement sécurisée combine systématiquement vérification de nonce, vérification de capacité, et un scope correct des hooks AJAX ou du permission_callback REST. C’est cette combinaison, et non un seul mécanisme isolé, qui constitue une vraie protection contre les requêtes forgées.

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