Un site WordPress n'est presque jamais la cible d'un attaquant qui l'a choisi. Il est la cible de programmes qui balayent Internet à la recherche de deux choses : des identifiants devinables et des extensions vulnérables. Une bonne stratégie de sécurité commence donc par comprendre ce que ces programmes cherchent, et par leur retirer l'information et les moyens dont ils ont besoin. C'est l'approche que nous avons suivie sur nos propres sites.

Le modèle de menace réaliste

Pour un site éditorial ou une petite boutique, la menace dominante n'est pas sophistiquée. Elle se résume à :

Chaque mesure qui suit répond à l'une de ces trois menaces. Ce qui ne répond à aucune relève du confort, pas de la sécurité.

1. Fermer l'énumération des comptes

Une force brute a besoin d'un identifiant. WordPress, dans sa configuration par défaut, le donne de plusieurs façons. Lors d'un audit de nos propres sites en septembre 2026, nous avons constaté sur l'un d'eux que :

Or, par défaut, le slug d'un auteur est dérivé de son identifiant de connexion. Ces trois canaux livrent donc la moitié du couple à deviner. Nous les avons fermés dans un module « must-use », chargé automatiquement sur tout le réseau et impossible à désactiver depuis l'administration :

// Énumération par l'API REST : la route reste ouverte aux utilisateurs
// authentifiés qui en ont le droit (l'éditeur de blocs en a besoin).
add_filter('rest_endpoints', function ($endpoints) {
    if (is_user_logged_in() && current_user_can('list_users')) {
        return $endpoints;
    }
    unset($endpoints['/wp/v2/users']);
    unset($endpoints['/wp/v2/users/(?P<id>[\d]+)']);
    return $endpoints;
});

// Énumération par ?author=N : redirection vers l'accueil.
add_action('template_redirect', function () {
    if (is_admin() || !isset($_GET['author'])) return;
    if (is_user_logged_in() && current_user_can('list_users')) return;
    wp_safe_redirect(home_url('/'), 301);
    exit;
});

// Fuite par oEmbed.
add_filter('oembed_response_data', function ($data) {
    unset($data['author_name'], $data['author_url']);
    return $data;
});

Deux détails comptent. D'abord, on ne ferme pas la route REST pour tout le monde : l'éditeur de blocs l'utilise pour afficher la liste des auteurs. Ensuite, on complète par un message d'échec de connexion neutre : le message par défaut distingue « identifiant inconnu » de « mot de passe incorrect », ce qui permet de valider un identifiant à coup sûr.

La vérification de ce durcissement nous a d'ailleurs réservé une surprise : le cache Nginx servait encore l'ancienne réponse de /wp-json/wp/v2/users après le déploiement. Le récit et la méthode de vérification sont dans notre guide sur le cache Nginx devant WordPress.

2. Couper XML-RPC s'il ne sert à rien

XML-RPC est l'ancienne interface de publication à distance de WordPress. Elle expose une méthode, system.multicall, qui permet d'enchaîner des centaines d'appels dans une seule requête HTTP — dont des tentatives de connexion. Toute limitation qui compte les requêtes, comme un limiteur Nginx, est alors contournée : une requête, cent essais.

Avant de couper, vérifiez que rien ne s'en sert : Jetpack et l'application mobile WordPress l'ont longtemps utilisée, certains outils de publication aussi. Sur nos sites, aucune extension n'en dépendait. Nous avons donc désactivé l'interface et retiré explicitement les méthodes à risque :

add_filter('xmlrpc_enabled', '__return_false');
add_filter('xmlrpc_methods', function ($methods) {
    unset($methods['system.multicall'], $methods['pingback.ping'],
          $methods['pingback.extensions.getPingbacks']);
    return $methods;
});

Le retrait de pingback.ping empêche en outre d'utiliser votre site comme relais pour des attaques par déni de service contre des tiers.

3. Limiter les tentatives au niveau du serveur

Une limitation de débit dans Nginx s'applique avant que PHP ne soit sollicité : elle protège à la fois contre la force brute et contre la surcharge qu'elle provoque. Sur nos serveurs, la page de connexion WordPress est limitée à une requête par seconde et par adresse, avec une rafale de cinq. Un humain ne s'en aperçoit jamais ; un script de force brute est ralenti d'un facteur considérable.

limit_req_zone $binary_remote_addr zone=wp_login:10m rate=1r/s;

location ~* /wp-login.php {
    limit_req zone=wp_login burst=5 nodelay;
    # … transmission à WordPress
}

Pensez à renvoyer un code 429 plutôt que le 503 par défaut, pour que ces refus ne se confondent pas avec des pannes dans vos outils : c'est l'objet de notre guide sur la limitation de débit et Googlebot.

Et bien sûr, la mesure la plus efficace contre la force brute reste hors de portée de tout script : des mots de passe longs et uniques pour chaque compte administrateur, et la double authentification.

4. Sortir les secrets du dépôt

Le fichier wp-config.php contient le mot de passe de la base de données et les clés de salage des sessions. S'il est versionné, il suffit d'un accès en lecture au dépôt — un ancien prestataire, un dépôt rendu public par erreur, une sauvegarde de poste de travail — pour les obtenir.

Notre configuration lit ses valeurs dans des variables d'environnement, et le fichier .env de production n'est pas versionné : il est stocké dans une variable protégée de l'outil d'intégration continue et écrit sur le serveur au moment du déploiement. Le dépôt peut alors être partagé sans rien divulguer. Si un secret a déjà été versionné, le retirer du dépôt ne suffit pas : il reste dans l'historique. Il faut le changer.

5. Un utilisateur de base de données par application

Nos sites WordPress et notre API Laravel partagent un même serveur MySQL, mais pas les mêmes identifiants : chaque application dispose de son propre utilisateur, qui n'a de droits que sur sa propre base. Une injection SQL dans une extension WordPress ne donne ainsi aucun accès aux données de l'API, et inversement. Cette séparation ne coûte rien à mettre en place au départ et beaucoup à rattraper ensuite.

Dans le même esprit, l'utilisateur WordPress n'a pas besoin des droits d'administration du serveur (GRANT, FILE, SUPER). Les droits sur sa base suffisent.

6. Mettre à jour, et savoir ce qui tourne

La majorité des compromissions de sites WordPress passent par une extension vulnérable pour laquelle un correctif existait. La discipline de mise à jour est donc la mesure la plus rentable de toutes — à condition de la rendre sûre : environnement de test, sauvegarde préalable, vérification après coup. Nous détaillons cette routine dans notre guide sur la maintenance d'un WordPress en production.

Corollaire : chaque extension installée est une surface d'attaque, même désactivée, car ses fichiers restent accessibles sur le serveur. Une extension qui ne sert plus se supprime.

7. Désactiver l'éditeur de fichiers intégré

L'administration WordPress permet de modifier le code des thèmes et des extensions depuis le navigateur. Un compte administrateur compromis devient ainsi, en quelques clics, un accès à l'exécution de code sur le serveur. Une constante supprime cette possibilité :

define( 'DISALLOW_FILE_EDIT', true );

Si vos mises à jour passent par un déploiement plutôt que par l'administration, DISALLOW_FILE_MODS va plus loin et interdit aussi l'installation d'extensions depuis l'interface.

Ce qui compte moins qu'on ne le dit

Ces mesures ne font pas de mal, mais elles ne doivent jamais passer avant les précédentes.

Liste de contrôle

  1. Énumération fermée : REST users, ?author=, oEmbed, message de connexion neutre.
  2. XML-RPC désactivé si rien ne s'en sert, multicall et pingback retirés dans tous les cas.
  3. Limitation de débit sur la connexion, au niveau du serveur, en 429.
  4. Mots de passe uniques et double authentification pour les administrateurs.
  5. Secrets hors du dépôt, et changés s'ils y ont été un jour.
  6. Un utilisateur de base par application, sans droits d'administration du serveur.
  7. Mises à jour régulières et extensions inutiles supprimées.
  8. DISALLOW_FILE_EDIT en production.