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.

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’unwp_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
Referercomme 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
- Un nonce est-il généré côté formulaire ou script, avec une action spécifique et non générique ?
- Ce nonce est-il vérifié côté serveur avec
check_admin_referer()oucheck_ajax_referer()? - 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 ? - L’action AJAX utilise-t-elle
wp_ajax_nopriv_alors qu’elle ne le devrait pas ? - La route REST associée définit-elle un
permission_callbackqui 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.