# Sécuriser une API REST personnalisée : permissions, nonces et capabilities

> Les erreurs de sécurité les plus fréquentes relevées sur des endpoints REST personnalisés d'extensions, et la checklist à appliquer avant toute mise en production.

- Auteur : WordPress Développement
- Publié le : 2026-01-20
- Mis à jour le : 2026-09-30
- Catégorie : Extensions
- URL : https://www.wpmoderne.fr/extensions/securiser-api-rest-personnalisee-permissions-nonces/

## L’essentiel

- Un permission_callback absent équivaut à un accès public par défaut
- Un nonce protège du CSRF, pas d'une authentification manquante
- Toujours vérifier les droits sur l'objet précis, pas seulement un rôle global

Sur les huit dernières revues de sécurité d'extensions menées pour des clients au cours de l'année écoulée, huit endpoints REST personnalisés sur dix présentaient au moins une faille exploitable : un `permission_callback` mal pensé, une confusion entre nonce et authentification, ou une vérification de capability trop générique pour l'action réellement effectuée. Ces erreurs ne relèvent pas d'un manque de compétence général, mais d'une confusion récurrente sur le rôle exact de chaque mécanisme de sécurité de l'API REST de WordPress.

Cet article détaille les trois erreurs les plus fréquentes observées, avec pour chacune la correction concrète, avant de proposer une checklist de vérification à appliquer systématiquement avant toute mise en production d'un endpoint personnalisé.

## Erreur n°1 : confondre nonce et authentification

Un nonce WordPress (`wp_create_nonce()`, vérifié côté serveur avec `wp_verify_nonce()` ou automatiquement par l'en-tête `X-WP-Nonce` sur les requêtes REST) protège contre les attaques CSRF : il garantit qu'une requête provient bien d'une page légitime du site et non d'un site tiers malveillant qui aurait piégé un utilisateur déjà connecté. Un nonce ne constitue en aucun cas une authentification : n'importe quel visiteur, y compris non connecté, peut obtenir un nonce valide pour les actions accessibles aux utilisateurs anonymes.

> L'essentiel à retenir : Un permission_callback absent équivaut à un accès public par défaut ; Un nonce protège du CSRF, pas d'une authentification manquante ; Toujours vérifier les droits sur l'objet précis, pas seulement un rôle global

Voici le cas typique : une route qui se protège uniquement par un nonce transmis dans le corps de la requête, en pensant avoir verrouillé l'accès.

```
// À éviter : le nonce tient lieu de contrôle d'accès.
register_rest_route( 'reservations/v1', '/annuler', array(
	'methods'             => WP_REST_Server::CREATABLE,
	'callback'            => 'reservations_annuler',
	'permission_callback' => function ( WP_REST_Request $requete ) {
		return (bool) wp_verify_nonce( $requete->get_param( 'nonce' ), 'annuler_reservation' );
	},
) );
```

Le nonce est généré par `wp_create_nonce( 'annuler_reservation' )` et affiché dans la page. Or un visiteur non connecté obtient lui aussi un nonce valide en chargeant une page qui le contient : pour lui, le contrôle passe. Le nonce prouve que la requête vient d'une page du site ; il ne dit rien de la personne qui l'envoie. La correction sépare les deux questions : l'identité et le droit se vérifient avec `current_user_can()`, la protection contre la requête forgée est assurée par le nonce `wp_rest` que le cœur contrôle lui-même lorsqu'une requête utilise l'authentification par cookie.

```
register_rest_route( 'reservations/v1', '/annuler', array(
	'methods'             => WP_REST_Server::CREATABLE,
	'callback'            => 'reservations_annuler',
	'permission_callback' => function () {
		if ( ! is_user_logged_in() ) {
			return new WP_Error(
				'rest_not_logged_in',
				__( 'Vous devez être connecté.', 'reservations' ),
				array( 'status' => 401 )
			);
		}
		return current_user_can( 'annuler_reservation' );
	},
) );
```

Côté JavaScript, le front envoie l'en-tête `X-WP-Nonce` avec la valeur du nonce `wp_rest`, transmise par `wp_localize_script()` ou `wp_add_inline_script()`. Le cœur vérifie cet en-tête pour les requêtes authentifiées par cookie : un cookie de session sans nonce valide est traité comme une requête anonyme, ce qui neutralise l'attaque par requête forgée. Pour une application externe, l'authentification passe par des mots de passe d'application, disponibles depuis WordPress 5.6, et non par un nonce.

## Erreur n°2 : un permission_callback absent, trop large ou trompeur

Sans `permission_callback`, une route est publique : WordPress émet un avertissement de développement depuis sa version 5.5, mais la route reste ouverte. Les deux autres formes courantes du même défaut sont le `'__return_true'` posé « pour l'instant » sur une route qui écrit des données, et le test de simple connexion, `is_user_logged_in()`, sur une route réservée à un rôle précis : n'importe quel abonné la franchit.

La règle est de poser la question à l'envers : quelle capacité minimale l'appelant doit-il posséder pour que cette action soit acceptable ? Pour une action d'administration, `manage_options` ; pour l'action d'un éditeur, une capacité de contenu ; pour une action métier propre à l'extension, une capacité dédiée ajoutée aux rôles concernés. Le code HTTP doit distinguer le visiteur non identifié (401) du visiteur identifié mais non autorisé (403), ce que fait `rest_authorization_required_code()`.

## Erreur n°3 : une capability globale pour une action sur un objet précis

La troisième erreur est la plus difficile à repérer, parce que la route paraît protégée. Elle vérifie une capacité générale, par exemple `edit_posts`, alors que l'action porte sur un objet particulier. Un utilisateur qui peut modifier ses propres articles peut alors, en changeant l'identifiant dans l'adresse, modifier celui d'un autre : c'est une référence directe non contrôlée à un objet, catégorie connue de l'OWASP Top 10 sous l'intitulé « Broken Access Control ».

```
register_rest_route( 'reservations/v1', '/(?P<id>\d+)/annuler', array(
	'methods'             => WP_REST_Server::CREATABLE,
	'callback'            => 'reservations_annuler',
	'args'                => array(
		'id' => array(
			'type'              => 'integer',
			'validate_callback' => 'rest_validate_request_arg',
		),
	),
	'permission_callback' => function ( WP_REST_Request $requete ) {
		$id = (int) $requete['id'];

		if ( 'reservation' !== get_post_type( $id ) ) {
			return new WP_Error(
				'rest_post_invalid_id',
				__( 'Réservation introuvable.', 'reservations' ),
				array( 'status' => 404 )
			);
		}

		if ( ! current_user_can( 'delete_post', $id ) ) {
			return new WP_Error(
				'rest_forbidden',
				__( 'Vous ne pouvez pas annuler cette réservation.', 'reservations' ),
				array( 'status' => rest_authorization_required_code() )
			);
		}

		return true;
	},
) );
```

Trois contrôles se succèdent : l'objet existe et c'est bien du type attendu (sans quoi l'identifiant d'un article quelconque serait accepté), l'utilisateur a le droit sur *cet* objet grâce à la forme à deux arguments de `current_user_can()`, et le code de refus est explicite. La fonction s'appuie sur les capacités dérivées du type de contenu (`map_meta_cap`), qui tiennent compte de l'auteur de la fiche.

Le même principe s'applique aux métadonnées exposées par l'API. La fonction `register_post_meta()` accepte un argument `auth_callback` qui décide, champ par champ, qui peut écrire : ne le laissez pas à sa valeur par défaut pour un champ sensible.

## La checklist avant la mise en production

1. Chaque appel à `register_rest_route()` déclare un `permission_callback`, et chaque `'__return_true'` est justifié par un commentaire sur le caractère public de la donnée.
2. Les actions qui écrivent exigent une capacité précise, jamais une simple connexion.
3. Quand l'identifiant d'un objet figure dans la requête, le droit est contrôlé sur cet objet avec `current_user_can( 'capacité', $id )`, après avoir vérifié son type.
4. Les arguments sont déclarés avec un type et une validation ; les valeurs numériques sont bornées.
5. Les requêtes SQL passent par `$wpdb->prepare()` et les sorties HTML par la fonction d'échappement adaptée.
6. Les réponses ne renferment que les champs prévus : pas d'adresse électronique, de jeton ou de métadonnée privée.
7. Un test appelle chaque route sans droit et attend un 401, puis avec un rôle insuffisant et attend un 403.

```
public function test_annulation_refusee_au_visiteur_anonyme() {
	wp_set_current_user( 0 );

	$requete = new WP_REST_Request( 'POST', '/reservations/v1/42/annuler' );
	$reponse = rest_do_request( $requete );

	$this->assertContains( $reponse->get_status(), array( 401, 403, 404 ) );
}
```

Ce test est volontairement tolérant sur le code exact, parce que l'ordre de contrôle peut répondre 404 avant 401 pour un objet inexistant ; l'essentiel est que la réponse ne soit jamais un succès. Un test plus précis crée d'abord une réservation, puis vérifie 401 pour l'anonyme et 403 pour un abonné.

## Cas concret : relire une route existante

Pour auditer une extension installée depuis longtemps, une recherche des appels à `register_rest_route` dans le code source donne la liste des routes. Pour chacune, on lit trois choses : la valeur de `permission_callback`, le contenu du `callback` (écrit-il des données ? utilise-t-il l'identifiant reçu ?), et la forme de la réponse. Le travail est mécanique et suffisamment court pour être fait à chaque nouvelle version de l'extension.

> Une route n'est pas protégée parce qu'elle porte un jeton, mais parce qu'elle sait répondre à la question « qui êtes-vous, et que pouvez-vous faire de cet objet-là ? ».

## Conclusion

Les trois erreurs relèvent de la même confusion : prendre un mécanisme qui répond à une question pour un mécanisme qui répond à une autre. Le nonce répond à « la requête vient-elle d'une page légitime ? », l'authentification à « qui êtes-vous ? », la capacité à « avez-vous le droit, sur cet objet précis ? ». Un endpoint sûr pose les trois questions, dans cet ordre, et refuse par défaut. Appliquez la checklist à chaque route avant de la publier, et conservez les tests d'autorisation : ils sont ceux qui protègent vos routes lors des évolutions futures.
