bald eagle door chain lock

Tutoriel : Protéger l’API REST WordPress avec un système de permissions flexible

🎯 Pourquoi sécuriser l’API REST WordPress : risques et enjeux

Comment sécuriser l’API REST WordPress efficacement ? Imaginez votre site WordPress comme une bibliothèque moderne où l’API REST permet de consulter le catalogue depuis l’extérieur. Ce tutoriel vous montre comment protéger votre API REST WordPress avec un système de permissions flexible et du code PHP prêt à l’emploi.

Le côté lumineux de l’API REST

Depuis un GROS moment, l’API REST est intégrée nativement et offre de superbes possibilités :

  • Développement d’applications mobiles connectées à votre site
  • Intégration avec des services externes
  • Manipulation des données WordPress via des requêtes HTTP simples
  • Création d’interfaces modernes avec React ou Vue.js

Le revers de la médaille

Mais voilà, cette accessibilité a un prix. Par défaut, certains endpoints (points d’accès) de l’API REST sont publics. Et c’est là que ça devient intéressant… ou inquiétant, c’est selon !

Prenons l’exemple des endpoints utilisateurs (/wp/v2/users). Sans protection particulière, ils peuvent exposer :

  • La liste de tous vos utilisateurs
  • Les noms d’affichage
  • Les identifiants en base de données
  • Et d’autres informations potentiellement sensibles

🚨 Endpoints WordPress vulnérables : analyse des risques de sécurité

Parlons cash : les endpoints de l’API REST WordPress, c’est un peu comme laisser les clés sur la porte. Pratique… mais risqué ! Examinons les points sensibles qui méritent votre attention immédiate.

Le cas critique des endpoints utilisateurs

L’endpoint /wp/v2/users est particulièrement sensible. Par défaut, il expose :

{
  "id": 42,
  "name": "samy",
  "url": "",
  "description": "",
  "link": "https://www.samy-kantari.fr/author/samy/",
  "slug": "captain",
  "avatar_urls": {
    "24": "https://secure.gravatar.com/avatar/f8e6178eec160c15488f4f1e6fede53e?s=24&d=mm&r=g",
    "48": "https://secure.gravatar.com/avatar/f8e6178eec160c15488f4f1e6fede53e?s=48&d=mm&r=g",
    "96": "https://secure.gravatar.com/avatar/f8e6178eec160c15488f4f1e6fede53e?s=96&d=mm&r=g"
  },
  "meta": [
    
  ],
  "_links": {
    "self": [
      {
        "href": "https://www.samy-kantari.fr/wp-json/wp/v2/users/42",
        "targetHints": {
          "allow": [
            "GET"
          ]
        }
      }
    ],
    "collection": [
      {
        "href": "https://www.samy-kantari.fr/wp-json/wp/v2/users"
      }
    ]
  }
}

🎯 Les risques concrets

L’accès public aux données utilisateurs expose votre site à de multiples risques : de l’énumération des comptes aux attaques ciblées, en passant par le social engineering et les tentatives de force brute – autant de portes ouvertes pour les personnes malintentionnées.

Autres endpoints sensibles

Les utilisateurs ne sont pas les seuls concernés. D’autres endpoints peuvent exposer :

  • /wp/v2/pages : la structure de votre site
  • /wp/v2/media : vos fichiers médias

Prenons l’exemple de /wp/v2/posts?status=draft : même si WordPress protège par défaut l’accès à vos brouillons, vérifier et contrôler explicitement ces permissions reste une bonne pratique – mieux vaut prévenir que guérir !

Et la liste s’allonge avec chaque plugin installé ou développement custom : autant d’endpoints supplémentaires qui nécessitent votre attention.

Les signaux d’alerte 🚩

Votre site est particulièrement vulnérable si :

  • L’API REST n’a jamais été sécurisée de votre côté
  • Des plugins exposent leurs propres endpoints sans restriction
  • WordPress ou vos plugins ne sont pas à jour régulièrement

Impact sur la sécurité

Du piratage de comptes à la non-conformité RGPD, les risques sont réels – mais la solution est à portée de main. Dans la suite, découvrez comment protéger efficacement vos endpoints en quelques lignes de code.

ℹ️ Bon à savoir

Les endpoints et signaux d’alerte mentionnés ici ne sont que la partie émergée de l’iceberg. Cet article vise avant tout à sensibiliser aux enjeux de sécurité de l’API REST WordPress. La liste complète des points de vigilance peut être bien plus longue selon votre configuration !

💡 Solution de sécurisation API REST WordPress : hook rest_authentication_errors

Tout repose sur un hook puissant : rest_authentication_errors.

Le principe

Ce hook est notre meilleur allié : il nous permet d’intercepter les requêtes vers l’API et de renvoyer une WP_Error si l’authentification échoue.

La logique est limpide :

  1. Si une erreur existe déjà → on la transmet
  2. Si l’endpoint ne nous intéresse pas → on laisse passer
  3. Sinon, on applique nos règles :
    • Utilisateur connecté ?
    • Rôle spécifique ?
    • Autres conditions ?

Et hop, une erreur est renvoyée si les conditions ne sont pas remplies !

👨‍💻 Code PHP pour sécuriser API REST WordPress

/**
 * Restreint l'accès à certains endpoints de l'API REST.
 *
 * @param mixed $errors Le résultat actuelle.
 * @return mixed WP_Error si l'accès est refusé, résultat original sinon.
 */
function sk_restrict_rest_access( $errors ) {
	if ( true !== $errors && is_wp_error( $errors ) ) {
		return $errors;
	}

	$current_route = isset( $GLOBALS['wp']->query_vars['rest_route'] )
		? $GLOBALS['wp']->query_vars['rest_route']
		: '';

	$protected_routes = array(
		'/wp/v2/users' => array(
			'condition'     => 'is_user_logged_in',
			'error_code'    => 'rest_user_cannot_view',
			'error_message' => __( 'Désolé, vous devez être connecté pour accéder à cette ressource.', 'sk-custom' ),
		),
	);

	$protected_routes = apply_filters( 'sk__restrict_rest_access_routes', $protected_routes );
	$protected_routes = sk_validate_protected_routes( $protected_routes );

	$route_to_check = false;
	foreach ( $protected_routes as $route => $config ) {
		if ( 0 === strpos( $current_route, $route ) ) {
			$route_to_check = $config;
			break;
		}
	}

	if ( ! $route_to_check ) {
		return $errors;
	}

	$has_access = false;
	if ( 'is_user_logged_in' === $route_to_check['condition'] ) {
		$has_access = is_user_logged_in();
	} elseif ( 'current_user_can' === $route_to_check['condition'] ) {
		$has_access = current_user_can( $route_to_check['capability'] );
	}

	if ( ! $has_access ) {
		return new WP_Error(
			$route_to_check['error_code'],
			$route_to_check['error_message'],
			array( 'status' => rest_authorization_required_code() )
		);
	}

	return $errors;
}
add_filter( 'rest_authentication_errors', 'sk_restrict_rest_access' );

WordPress nous facilite la vie avec ses variables globales ! Ici, on récupère la route actuelle via $GLOBALS[‘wp’]->query_vars[‘rest_route’].

$current_route = isset( $GLOBALS['wp']->query_vars['rest_route'] )
  ? $GLOBALS['wp']->query_vars['rest_route']
  : '';

Cette petite ligne de code nous permet d’accéder directement à l’URL de l’API REST appelée. Plutôt pratique, non ? 😎

$protected_routes = array(
  '/wp/v2/users' => array(
    'condition'     => 'is_user_logged_in',
    'error_code'    => 'rest_user_cannot_view',
    'error_message' => __( 'Désolé, vous devez être connecté pour accéder à cette ressource.', 'sk-custom' ),
  ),
);

Décortiquons ce tableau :

  • La clé /wp/v2/users cible l’endpoint de listing des utilisateurs
  • condition : on vérifie si l’utilisateur est connecté via is_user_logged_in
  • error_code : un code d’erreur explicite pour l’API
  • error_message : le message que verront les utilisateurs non autorisés

Simple et efficace : pas connecté = pas d’accès à la liste des utilisateurs ! 🚫

🎯 Des conditions flexibles

Dans notre configuration, le champ condition accepte deux types de vérification :

'condition' => 'is_user_logged_in'  // L'utilisateur doit être connecté
// OU
'condition' => 'current_user_can'   // L'utilisateur doit avoir une capacité spécifique

C’est vous qui choisissez ! Selon vos besoins :

  • is_user_logged_in : parfait pour une simple authentication
  • current_user_can : idéal pour un contrôle plus fin par rôle ou capacité

On verra ensuite comment le code traite ces deux cas différemment. 🔄

🤝 Pensons communauté !

$protected_routes = apply_filters( 'sk__restrict_rest_access_routes', $protected_routes );

Un filtre WordPress bien placé et hop ! N’importe quel développeur peut enrichir notre système de protection. C’est la force de l’écosystème WordPress : construire des solutions qu’on peut facilement étendre et adapter.

🛡️ Confiance mais contrôle !

$protected_routes = sk_validate_protected_routes( $protected_routes );

On ouvre les portes de notre code avec le filtre, certes, mais on vérifie que tout le monde s’essuie les pieds avant d’entrer ! Cette validation nous assure que :

  • Chaque route a une structure valide
  • Les conditions sont conformes à nos attentes
  • Les paramètres obligatoires sont présents

C’est la règle d’or : soyez généreux dans ce que vous acceptez, mais strict dans ce que vous traitez ! 🎯

👀 Je parie que vous attendez le code de validation… Le voici !

/**
 * Valide et normalise le tableau des routes protégées.
 *
 * @param array $routes Tableau des routes à protéger.
 * @return array Tableau normalisé des routes.
 */
function sk_validate_protected_routes( $routes ) {
	if ( ! is_array( $routes ) ) {
		return array();
	}

	$validated_routes = array();

	foreach ( $routes as $route => $config ) {
		if ( ! is_string( $route ) || empty( $route ) ) {
			continue;
		}

		if ( ! is_array( $config ) ) {
			continue;
		}

		$validated_routes[ $route ] = sk_normalize_route_config( $config );
	}

	return $validated_routes;
}

🛠️ Validation en deux temps

D’abord les contrôles de base :

  • On vérifie qu’on a bien un tableau en entrée
  • On contrôle la structure de chaque route

Et pour finir en beauté ? On normalise toute la configuration avec une autre méthode que vous allez adorer… 😎 ( Ou pas. On ne juge pas ici. Certains aiment Star Wars, d’autres préfèrent Star Trek 🙃 )

/**
 * Normalise la configuration d'une route protégée.
 *
 * @param array $route_config Configuration de la route.
 * @return array Configuration normalisée.
 */
function sk_normalize_route_config( $route_config ) {
	$defaults = array(
		'condition'     => 'is_user_logged_in',
		'capability'    => '',
		'error_code'    => 'rest_access_denied',
		'error_message' => __( 'Accès non autorisé.', 'sk-custom' ),
	);

	$config = wp_parse_args( $route_config, $defaults );

	if ( ! in_array( $config['condition'], array( 'is_user_logged_in', 'current_user_can' ), true ) ) {
		$config['condition'] = $defaults['condition'];
	}

	if ( 'current_user_can' === $config['condition'] && empty( $config['capability'] ) ) {
		$config['capability'] = 'read';
	}

	return $config;
}

🎯 La normalisation, simple mais efficace !

Tout commence avec notre meilleur ami wp_parse_args() – une fonction WordPress qui fait le travail ingrat pour nous : fusionner nos données avec un schéma par défaut.

Ensuite, on joue aux gendarmes avec la condition :

  • Elle doit exister (non négociable !)
  • Deux choix possibles : is_user_logged_in ou current_user_can
  • Si c’est autre chose ? On bascule sur is_user_logged_in par défaut (on est sympas)

Et petit bonus pour current_user_can :

  • Pas de capability vide acceptée (on n’est pas des sauvages)
  • Si elle manque ? On force « read » (le minimum syndical)

🔄 La logique finale : simple comme bonjour !

Étape 1 : Le contrôle de route

On vérifie si la route actuelle fait partie de notre liste de routes protégées.

  • Si non → Circulez, y’a rien à voir !
  • Si oui → On passe aux vérifications

Étape 2 : Les vérifications d’accès

Selon la condition configurée :

  • is_user_logged_in : « T’es connecté ou tu passes pas ! »
  • current_user_can : « Montre-moi tes permissions ! »

Étape 3 : Le verdict

En cas de refus → Une belle WP_Error avec :

  • Code 401 si vous n’êtes pas connecté 🚫
  • Code 403 si vous n’avez pas les droits 🔒

Et si tout est OK ? On laisse passer tranquillement ! ✨

WordPress nous facilite la vie avec rest_authorization_required_code() qui choisit le bon code d’erreur selon le contexte.

🦥 Pour les amateurs d’efficacité (ou les flemmards assumés)

Tadaaa ! Voici le code complet, testé et approuvé (en place sur le blog ou peut-être pas 🤷‍♂️). Parce que parfois, la meilleure ligne de code est celle qu’on n’a pas à écrire :

/**
 * Normalise la configuration d'une route protégée.
 *
 * @param array $route_config Configuration de la route.
 * @return array Configuration normalisée.
 */
function sk_normalize_route_config( $route_config ) {
	// Configuration par défaut
	$defaults = array(
		'condition'     => 'is_user_logged_in',
		'capability'    => '',
		'error_code'    => 'rest_access_denied',
		'error_message' => __( 'Accès non autorisé.', 'sk-custom' ),
	);

	// Fusion avec les valeurs par défaut
	$config = wp_parse_args( $route_config, $defaults );

	// Validation de la condition
	if ( ! in_array( $config['condition'], array( 'is_user_logged_in', 'current_user_can' ), true ) ) {
		$config['condition'] = $defaults['condition'];
	}

	// Si la condition est current_user_can, vérifie que capability est défini
	if ( 'current_user_can' === $config['condition'] && empty( $config['capability'] ) ) {
		$config['capability'] = 'read'; // Capacité par défaut
	}

	return $config;
}

/**
 * Valide et normalise le tableau des routes protégées.
 *
 * @param array $routes Tableau des routes à protéger.
 * @return array Tableau normalisé des routes.
 */
function sk_validate_protected_routes( $routes ) {
	if ( ! is_array( $routes ) ) {
		return array();
	}

	$validated_routes = array();

	foreach ( $routes as $route => $config ) {
		// Vérifie que la route est une chaîne valide
		if ( ! is_string( $route ) || empty( $route ) ) {
			continue;
		}

		// Vérifie que la configuration est un tableau
		if ( ! is_array( $config ) ) {
			continue;
		}

		// Normalise la configuration
		$validated_routes[ $route ] = sk_normalize_route_config( $config );
	}

	return $validated_routes;
}

/**
 * Restreint l'accès à certains endpoints de l'API REST.
 *
 * @param mixed $errors Le résultat de l'authentification actuelle.
 * @return mixed WP_Error si l'accès est refusé, résultat original sinon.
 */
function sk_restrict_rest_access( $errors ) {
	// Si déjà une erreur, on la retourne.
	if ( true !== $errors && is_wp_error( $errors ) ) {
		return $errors;
	}

	// Récupère la route actuelle.
	$current_route = isset( $GLOBALS['wp']->query_vars['rest_route'] )
		? $GLOBALS['wp']->query_vars['rest_route']
		: '';

	// Liste des routes �� protéger avec leurs conditions.
	$protected_routes = array(
		'/wp/v2/users' => array(
			'condition'     => 'is_user_logged_in',
			'error_code'    => 'rest_user_cannot_view',
			'error_message' => __( 'Désolé, vous devez être connecté pour accéder à cette ressource.', 'sk-custom' ),
		),
	);

	// Applique le filtre et valide les routes
	$protected_routes = apply_filters( 'sk__restrict_rest_access_routes', $protected_routes );
	$protected_routes = sk_validate_protected_routes( $protected_routes );

	// Vérifie si la route actuelle doit être protégée.
	$route_to_check = false;
	foreach ( $protected_routes as $route => $config ) {
		if ( 0 === strpos( $current_route, $route ) ) {
			$route_to_check = $config;
			break;
		}
	}

	// Si la route n'est pas dans notre liste, on retourne le résultat original.
	if ( ! $route_to_check ) {
		return $errors;
	}

	// Vérifie la condition d'accès.
	$has_access = false;
	if ( 'is_user_logged_in' === $route_to_check['condition'] ) {
		$has_access = is_user_logged_in();
	} elseif ( 'current_user_can' === $route_to_check['condition'] ) {
		$has_access = current_user_can( $route_to_check['capability'] );
	}

	// Si l'accès est refusé, retourne une erreur.
	if ( ! $has_access ) {
		return new WP_Error(
			$route_to_check['error_code'],
			$route_to_check['error_message'],
			array( 'status' => rest_authorization_required_code() )
		);
	}

	return $errors;
}
add_filter( 'rest_authentication_errors', 'sk_restrict_rest_access' );

⚡️ Bonus : Il est commenté et tout propre, parce qu’on n’est pas des barbares quand même !

🧙‍♂️ MAIS ATTENTION ! (Oui, les majuscules sont nécessaires)

Comme dirait l’oncle Ben : « Un grand code implique de grandes responsabilités » (ou un truc du genre). Alors avant de copier-coller comme un Ninja de la productivité, rappelez-vous que :

  • Comprendre le code, c’est comme lire la notice d’un meuble IKEA : c’est conseillé si vous ne voulez pas vous retrouver avec une API bancale
  • Même si ce code est aussi robuste que Thor avec son marteau, la sécurité c’est comme les mises à jour Windows : on ne peut pas les ignorer éternellement
  • Testez, adaptez, comprenez… et après seulement, savourez !

Et si vous pensez que je suis un poil parano avec la sécurité, rappelez-vous que même Batman double-vérifie son Batcape 😅 avant de sauter des immeubles.🦇

PS : Ce message d’auto-dérision vous est offert par un développeur qui a appris à ses dépens qu’un copier-coller irréfléchi peut transformer une journée tranquille en marathon de debugging… 😅

Samy Kantari - Expert WordPress + IA

Kantari Samy

Expert WordPress + IA

👨‍💻 10 ans dans le game WordPress, chez Whodunit, à bricoler du code, à dompter des bugs et à faire tourner des projets de toutes tailles.
Puis l’IA est arrivée… et là, révélation 💡 !
J’ai switché de mindset, réinventé ma façon de coder et avec le vibe coding : une nouvelle ère où je ne suis plus limité par le temps ni par les outils.

Aujourd’hui ? Je code toujours… Mais avec mon copilote IA.
On forme une team de choc. Lui, c’est la puissance. Moi, c’est la vision. Ensemble, on déverrouille ce qui semblait impossible hier. 🚀

10+ Années d'expérience
+++ Projets réalisés
80% code par IA
S’abonner
Notification pour
guest
3 Commentaires
Commentaires en ligne
Afficher tous les commentaires
Quentin

Bonjour Samy 🙂
Merci pour cet article hyper intéressant !
Dans toutes mes modestes tentatives d’ajouter une couche de sécurité à l’API REST de WordPress, j’ai toujours fini par constater des soucis de compatibilité avec d’autres extensions :

  • Blocs filtres natifs de WooCommerce ne fonctionnant plus en frontend.
  • Tableau de bord (accueil) de WooCommerce dans les choux.
  • Nouvelle interface de traduction de WPML qui ne fonctionne pas.
  • Nouvelles fonctionnalités de suggestion de texte basées sur « l’IA » de Yoast SEO qui ne fonctionnent pas.

J’avoue ne pas avoir tout bien compris dans ton code, mais j’imagine qu’il faut pouvoir identifier les routes utilisées par ces extensions pour les laisser « libres » ?

Merci de ton retour, a + !

Quentin

Typiquement, tu peux tester avec WooCommerce et un thème basé sur les blocs en front-end en étant déconnecté. Sur une page d’archive de produits (boutique ou autre), si tu as le bloc filtre d’utilisé, tu verras si ton renforcement de sécurité sur l’API REST casse la fonctionnalité. 😉
Je garde ton code de côté pour essayer aussi dès que j’ai un moment !
Bon dimanche !