# 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.

- Auteur : Clément Hadrot
- Publié le : 2021-11-10
- Mis à jour le : 2021-11-10
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/csrf-extensions-au-dela-nonces/

## L’essentiel

- 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

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.
