Sécurité WordPress : mes pratiques et références

J’utilise souvent les mêmes éléments, et je m’y perds un peu alors autant tout centraliser ici, comme un wiki.

Et que cela serve à d’autres si jamais.

Standards de sécurisation WordPress illustration

Introduction

WordPress est l’un des gestionnaires de contenu les plus utilisés, et à fortiori les plus ciblés par les pirates informatiques. Pourtant, certaines précautions ne sont pas très chronophages et vous mettent à l’abri pendant pas mal de temps des soucis d’intrusion.

A travers ce guide, je vous partage une partie de mes protocoles de sécurisation WP. Si vous souhaitez que je réalise un audit de votre site ou que je le sécurise à votre place, faites-le moi savoir.

Contenu par défaut des fichiers sensibles WordPress

Je travaille depuis 2011 sur WordPress, pour des projets personnels, collaboratifs ou professionnels. A force de tripoter, casser et refaire, j’ai appris certaines choses, une forme d’expérience par l’expérimentation.

Certains fichiers sont sensibles et demandent à être bichonnés, je veux bien entendu parler du wp-config.php et du fichier .htaccess à la racine du site.

Fichier wp-config.php

Comme je suis un peu maniaque et que j’apprécie de m’y retrouver, je reprends à chaque fois le contenu du fichier wp-config.php afin de le clarifier.

  • Augmentation de la mémoire PHP
  • Affichage clair des éléments relatifs à la base de données
  • Mise-à-jour du SALT en cas d’infection
  • Options de debug
  • Blocage de l’édition de fichiers
  • Blocage du wp-cron.php
<?php

// PHP
define( 'WP_MEMORY_LIMIT', '512M' );
define( 'WP_MAX_MEMORY_LIMIT', '512M' );

// Cache
define( 'WP_CACHE', false);

// DB
define('DB_NAME', 'XXXXX');
define('DB_USER', 'XXXXX');
define('DB_PASSWORD', 'XXXXX');
define('DB_HOST', 'XXXXX');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
$table_prefix  = 'XXXXX_';

// SALT
define('AUTH_KEY',         ';W_+|:9?2.ZONuF4bzSuCOeXBUpHp=-T~EmX]%9PF5&R+1v4YMj{sn-ki[+y<[&@');
define('SECURE_AUTH_KEY',  '!R<#MA2zT^1ceWQR6:uI6P+]KJ[P!IPCrpB+>>03lP)FdHd^(V-,6^|L}PMc+yV7');
define('LOGGED_IN_KEY',    'p5U$qno,Q>=B-}}V|wf6F.czvdcK#}>OfEAf0aC5V^G/;e{.|>X/o~}LnA#TG$(C');
define('NONCE_KEY',        'QTEc8(B~Q*G5Re/qV)+/.B/v7{Ic(g#rIvIi1Z=X|AvXw=scCY%-XvC(@U$K0)9O');
define('AUTH_SALT',        'fsbDi+^u;jcI+C|68.,`CtyF#k)zpi_NHtU|v{eYHzD*ehQu;YID02qGc-2=k)8(');
define('SECURE_AUTH_SALT', ',j=s0<SCRQc6+^$5ZVAM)G1>R>O(TBi@:n/BuEPzdY;KbP+in-a/mNm9-h|#fM%6');
define('LOGGED_IN_SALT',   'e/A=Nx7dZ)vYk5cW]a]1aM-29r~+<dzj7r#E[|Brd2Cu+%]A3@ww&lcwX2iw@]-l');
define('NONCE_SALT',       'EzqOaJtsn<~U>;J/$i!;{/uGVHbK?SJBYTf H|`sNb3Zxe_lUr0! Bc-5,xDlV$d');

// Debug
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);
define('WP_DEBUG_DISPLAY', false);

// Sécurité
define('DISALLOW_FILE_EDIT', true);
define('FORCE_SSL_ADMIN', true);
define('DISABLE_WP_CRON', true);

// WP
if ( !defined('ABSPATH') ) define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');

Fichier .htaccess

J’ajoute dans ce fichier .htaccess, présent à la racine du site, plusieurs éléments, notamment pour réduire la surface d’attaque potentielle :

  • Redirection automatique vers HTTPS
  • Désactivation du listing du contenu des dossiers
  • Protection des fichiers sensibles type wp-config.php wp-cron.php xmlrpc.php
  • Protection des fichiers commençant par un .
  • Protection du fichier readme

# Symlinks et désactivation du listing des répertoires
Options +FollowSymLinks -Indexes

# Masquage des informations serveur
ServerSignature Off

# Protection des fichiers sensibles WordPress
<Files "wp-config.php">
    Require all denied
</Files>
<Files "xmlrpc.php">
    Require all denied
</Files>
<Files "wp-cron.php">
    Require all denied
</Files>

# Protection des fichiers .htaccess et .htpasswd
<FilesMatch "^\.ht">
    Require all denied
</FilesMatch>

# Protection readme, license, changelog
<FilesMatch "(?i)^(readme\.(html|txt|md)|changelog\.txt|licen[cs]e(\.txt)?)$">
    Require all denied
</FilesMatch>

# Protection des fichiers de sauvegarde et logs
<FilesMatch "\.(sql|bak|log|tar|gz|zip|old)$">
    Require all denied
</FilesMatch>

# Règles de réécriture
<IfModule mod_rewrite.c>
    RewriteEngine On

    # Redirection HTTPS (proxy/CDN + accès direct)
    RewriteCond %{HTTP:X-Forwarded-Proto} !https
    RewriteCond %{HTTPS} off
    RewriteRule ^(.*)$ https://%{HTTP_HOST}/$1 [R=301,L]

    # Masquage des pages auteur (?author=N)
    RewriteCond %{QUERY_STRING} ^author=[0-9]+
    RewriteRule .* - [F,L]

    # Blocage injections XSS et base64 via query string
    RewriteCond %{QUERY_STRING} (<|%3C).*script.*(>|%3E) [NC,OR]
    RewriteCond %{QUERY_STRING} base64_encode.*\(.*\) [NC]
    RewriteRule .* - [F,L]

</IfModule>

Sécurisation de base d’une instance WordPress

Voici une liste non exhaustive de ce que je réalise pour prévenir les attaques potentielles sur les sites WP que je gère : extensions, thèmes, administrateurs, fichiers sensibles, base de données…

Extensions

Parmi les extensions que j’utilise fréquemment, il y a de manière non exhaustive :

  • Wordfence security : scan avec fréquence variable, firewall, 2FA disponible dès la version gratuite
  • WPS Hide login ou équivalent : déplacement de l’URL de connexion pour la personnaliser et faciliter sa mémorisation par des néophytes
  • Cloudflare : WAF et protection captcha (Turnstile)
  • SEOPress ou équivalent : basiques pour le référencement naturel
  • Smush ou équivalent : compression des images, voire conversion au format .webp
  • UpdraftPlus ou équivalent : sauvegarde locale du site (FTP + BD)
  • Redirection : gestion des redirections, possiblement automatiquement
  • Broken Link Checker : contrôle des liens morts et notification mail, option bulk disponible

De manière générale, je recommande de regarder si une extension est pérenne (mise à jour fréquemment), fiable (avis utilisateurs) et pertinente (sans possibilité de passer par un mu-plugin ou un snippet dans functions.php) avant de valider son installation. On se préserve ainsi d’extensions inutiles, nombreuses voire sans réelle valeur ajoutée, et on réduit les coûts de maintenance.

Thèmes

Je ne laisse que deux thèmes disponibles : le thème actif et un thème de fallback de la famille Twenty.

Cela limite la maintenance (car un thème pas à jour, même désactivé, est une brèche potentielle) et le poids du site.

D’autre part, en cas de bug, je renomme le fichier du thème actif pour que le thème Twenty prenne le relais, me permettant ainsi de voir si le thème actif est la source du problème.

Suppression de l’admin #1 et sécurisation des Utilisateurs administrateurs

Je supprime systématiquement le premier utilisateur admin WordPress (admin 1) pour des raisons de sécurité. C’est le premier et souvent seul utilisateur ciblé par des scripts malveillants.

Par la suite, je limite au maximum le nombre d’administrateurs sur un site, car peu utilisent réellement toutes les fonctionnalités de ce rôle utilisateur.

Enfin, je force pour que chaque administrateur ait une connexion via 2FA via Wordfence (double authentification) et un mot de passe solide.

Protection des fichiers sensibles wp-config.php xmlrpc.php via .htaccess

Grâce à un fichier .htaccess optimisé, je m’assure que l’accès aux fichiers wp-config.php et xmlrpc.php est interdit

<Files wp-config.php>
    Order deny,allow
    Deny from all
</Files>
<Files xmlrpc.php>
    Order deny,allow
    Deny from all
</Files>

Le cas particulier de wp-cron.php

Je n’utilise pas wp-cron.php, accessible publiquement par défaut, car il est exploitable pour des attaques DDoS ou des tâches malveillantes. Cependant, la tâche est assez ardue, voilà pourquoi je fais ce mémo.

Le principe de ma méthode est de bloquer l’accès public au fichier wp-cron.php via le .htaccess, puis de le remplacer par un pont déclenchant les tâches via l’API interne de WordPress tout en protégeant ce fichier par un token secret.

Je précise que cette méthode est configurable quel que soit l’hébergement, même si vous devrez possiblement faire quelques ajustement, notamment sur la longueur du token, en fonction de votre hébergement.

Voici les 5 étapes à réaliser pour désactiver le WP CRON et créer un CRON maison en déclenchant son utilisation via une tâche CRON générée par votre hébergement.

define('DISABLE_WP_CRON', true);
  • Désactivation de l’accès au fichier wp-cron.php via .htaccess
<Files wp-cron.php>
    Order deny,allow
    Deny from all
</Files>
  • Création du pont à nommer comme vous le souhaitez, par exemple run-cron.php, et à ajouter à la racine de votre site WordPress.
<?php
/**
 * Fichier pont run-cron.php pour exécuter les tâches WP-Cron de manière sécurisée.
 * Remplace l'appel direct à wp-cron.php.
 */

// 1. Vérification du token secret (Sécurité applicative)
// Remplacez cette chaîne par votre token unique de 60 caractères (alphanumérique)
$secret = 'VOTRE_TOKEN_ALPHANUMERIQUE_DE_40_CARACTERES_ICI';

if (!isset($_GET['key']) || $_GET['key'] !== $secret) {
    http_response_code(403);
    exit('Accès interdit : Clé manquante ou invalide.');
}

// 2. Chargement de l'environnement WordPress
define('WP_USE_THEMES', false);
require_once __DIR__ . '/wp-load.php';

// 3. Exécution manuelle des tâches en attente
// Équivalent PHP de la commande "wp cron event run --due-now"
$crons = _get_cron_array();
$gmt_time = microtime(true);

if (!empty($crons)) {
    foreach ($crons as $timestamp => $cronhooks) {
        // Ignorer les tâches futures
        if ($timestamp > $gmt_time) break;
        
        foreach ($cronhooks as $hook => $keys) {
            foreach ($keys as $k => $v) {
                $schedule = $v['schedule'];
                
                // Rescheduler si récurrent
                if ($schedule) {
                    wp_reschedule_event($timestamp, $schedule, $hook, $v['args']);
                }
                
                // Déscheduler l'occurrence actuelle
                wp_unschedule_event($timestamp, $hook, $v['args']);
                
                // Exécuter la tâche
                do_action_ref_array($hook, $v['args']);
            }
        }
    }
    echo "Tâches cron exécutées avec succès via WP interne.\n";
} else {
    echo "Aucune tâche en attente.\n";
}
?>
  • Configuration d’un token secret de 40 caractères par exemple, incluant des lettres majuscules et minuscules et des chiffres (pas de caractères spéciaux). Ajoutez ce token dans le contenu du fichier run-cron.php à l’emplacement indiqué.
  • Création d’une tâche CRON depuis votre hébergement en indiquant notamment :
    • URL du CRON : https://votre-domaine.com/run-cron.php?key=VOTRE_TOKEN_ALPHANUMERIQUE_DE_40_CARACTERES_ICI
    • Fréquence : 15 minutes à 1 heure en fonction des hébergements
    • Sécurité complémentaire : authentification HTTP (digest ou basic) si possible et disponible

Une fois les fichiers wp-config.php, .htaccess et run-cron.php mis en place comme il se doit, vous pouvez alors tester que le wp-cron.php est inaccessible et que le run-cron.php fait bien son travail.

  • LURL https://votre-domaine.com/wp-cron.php doit donner une erreur 403 : accès interdit.
  • L’URL https://votre-domaine.com/run-cron.php?key=VOTRE_TOKEN_ALPHANUMERIQUE_DE_40_CARACTERES_ICI doit afficher un message Tâches cron exécutées avec succès via WP interne ou Aucune tâche en attente.

Déplacement du fichier wp-config.php

Tout en l’ayant personnalisé, je remonte le wp-config.php d’un cran dans l’arborescence WordPress.

En effet, wp-config.php est par défaut dans le dossier racine, ce qui pourrait être compromettant en cas d’intrusion. Vous pouvez le remonter d’un niveau sans problématiques majeures.

Il faudra peut-être le remettre à sa place par défaut temporairement dans certains cas (extension de gestion de cache qui veut l’utiliser pour écrire des éléments).

Base de données originale et personnalisée

Je personnalise la base de données de plusieurs manières, l’idée étant qu’il n’y ait pas de pattern.

  • Nom de la base et nom d’utilisateur différents, chose possible sur des hébergements type O2switch mais impossibles chez Infomaniak ou OVH
  • Mot de passe unique et complexe, généré par des outils comme Bitwarden
  • Préfixe de la base personnalisé

Des règles de base

A cette liste s’ajoutent des règles de logique et de personnalisation en fonction des outils utilisés :

  • Limitation du nombre de connexions infructueuses, possiblement limitation par IP ou par pays
  • Masquage de la version de WordPress
  • Mises-à-jour régulières pour une maintenance préventive efficace
  • Thèmes et extensions non utilisés, désactivés ou abandonnés à supprimer
  • Désactivation de l’API REST
  • Blocage de l’énumération des utilisateurs (paramètre Wordfence ou script PHP)…

Conclusion

Aucun site n’est infaillible et aucune sécurisation ne sera définitive sur le long terme, il s’agit avant tout d’avoir une base solide et de limiter la surface d’attaque.

Avec ces quelques éléments, vous devriez déjà être plus tranquilles. Au besoin, contactez-moi.