🎯 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 :
- Si une erreur existe déjà → on la transmet
- Si l’endpoint ne nous intéresse pas → on laisse passer
- 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… 😅