Si vous avez déjà vu passer dans vos logs des requêtes vers admin-ajax.php?action=... ou wp-json/ à un rythme absurde, vous avez déjà été “dans la zone” où les alertes Attackers Actively Exploiting Critical deviennent concrètes.
Le point dur à accepter : ce type d’alerte ne décrit pas une attaque “ciblée”. Elle décrit une industrialisation. Des bots scannent, trouvent un plugin vulnérable, enchaînent l’exploit, déposent un webshell, ajoutent un admin, puis monétisent (spam SEO, redirections, minage, vol de données).
Contexte : avril 2026, WordPress 6.9.4 et PHP 8.1+. Le cœur WordPress est généralement robuste quand il est à jour. Les compromissions que je traite le plus souvent viennent d’un plugin (ou d’un mu-plugin “maison”) qui a pris un raccourci sur la validation d’entrée, les permissions, ou la sécurité REST/AJAX.
La menace
Quand une faille critique est activement exploitée, l’attaquant n’essaie pas “d’entrer” au hasard : il exécute une séquence automatisée qui vise un résultat précis. Sur WordPress, les scénarios réels que je vois le plus en incident sont :
- Création d’un compte administrateur (souvent via une route REST mal protégée, ou un endpoint AJAX sans contrôle de capacité).
- Injection de contenu (spam SEO dans des articles existants, ou création de pages “casino/pharma”).
- Redirection furtive (JS injecté uniquement pour les visiteurs mobiles ou via un referer Google).
- Exfiltration (vol d’emails, de tokens, parfois de données WooCommerce si le site est e-commerce).
- Persistance (webshell dans
wp-content/uploads/, tâche cron ajoutée, mu-plugin injecté).
La fréquence ? Les chiffres varient selon les sources et l’écosystème, mais un point est stable : la majorité des tentatives sont opportunistes et massives. Le guide officiel de durcissement WordPress insiste justement sur le fait que la surface d’attaque est surtout dans les extensions, thèmes et configurations.
En langage simple : si une extension expose une action (AJAX/REST) qui accepte des paramètres non filtrés, et qu’elle oublie d’exiger une permission (capability) + un nonce quand c’est nécessaire, un bot peut appeler cette action comme un utilisateur anonyme. Et si cette action écrit un fichier, modifie une option, ou crée un utilisateur… vous avez une compromission.
Pourquoi “critique” devient “catastrophique” sur WordPress
WordPress a un modèle de permissions solide (current_user_can(), rôles/capabilités), une protection CSRF via nonces, et des APIs de validation/sanitation. Le problème vient de la colle : le code autour, souvent écrit vite.
J’ai souvent croisé ce problème sur des sites qui utilisent des snippets copiés d’anciens tutoriels (pré-REST moderne), collés dans un plugin de snippets, et jamais révisés quand le site est passé à PHP 8.1+ et WordPress 6.9.x.
Résumé rapide
- Mettez à jour WordPress 6.9.4, les plugins/thèmes, et supprimez ce qui n’est pas utilisé (désactivé ≠ inoffensif).
- Auditez vos endpoints AJAX (
wp_ajax_*) et REST (register_rest_route) : permissions + validation stricte + nonces si contexte navigateur. - Bloquez l’exécution PHP dans
wp-content/uploads(et idéalement dans d’autres dossiers d’assets). - Activez des en-têtes HTTP (CSP basique, X-Frame-Options/Frame-Ancestors, nosniff) et forcez HTTPS.
- Surveillez les logs : pics sur
admin-ajax.php,wp-json, tentatives surwp-content/uploads/*.php, création d’admins. - Si compromis : isolez, sauvegardez pour forensique, réinstallez le core, remplacez plugins/thèmes depuis sources officielles, faites tourner des scans, changez tous les secrets.
Code vulnérable — ce qu’il ne faut PAS faire
Le pattern le plus dangereux que je vois en 2026 : un endpoint REST “pratique” (souvent pour Elementor/Divi/Avada ou un front-end custom) qui permet d’enregistrer une option, de générer un fichier CSS/JS, ou de pousser un template… sans permission stricte.
Exemple volontairement vulnérable : un plugin “maison” qui génère un fichier CSS dans uploads à partir d’un paramètre JSON. L’intention est bonne (perfs, cache), mais l’implémentation ouvre une porte.
<?php
/**
* Plugin Name: BPCAB Custom CSS (VULNÉRABLE - EXEMPLE)
*/
add_action('rest_api_init', function () {
register_rest_route('bpcab/v1', '/save-css', [
'methods' => 'POST',
'callback' => 'bpcab_save_css_vulnerable',
// DANGER : permission_callback trop permissive
'permission_callback' => '__return_true',
]);
});
function bpcab_save_css_vulnerable(WP_REST_Request $request) {
// DANGER : aucune validation stricte, aucune sanitation
$css = $request->get_param('css');
// DANGER : écriture dans uploads sans contrôle, nom de fichier prévisible
$upload_dir = wp_upload_dir();
$target = trailingslashit($upload_dir['basedir']) . 'bpcab-custom.css';
// DANGER : pas de contrôle d'erreur, pas de verrouillage, pas de limites
file_put_contents($target, $css);
return new WP_REST_Response([
'ok' => true,
'file' => $target,
], 200);
}
Comment l’attaque fonctionne (sans “recette” d’exploitation)
Voici ce qui se passe en coulisses :
- Le bot scanne les routes REST publiques (
wp-json) et repère/bpcab/v1/save-css. - Comme
permission_callbackrenvoie vrai, l’endpoint est appelable anonymement. - L’attaquant injecte un contenu volumineux (DoS disque) ou un contenu malveillant si le fichier est ensuite servi au navigateur (ex : CSS exfiltration ou chargement indirect).
“Ce n’est que du CSS” est un piège classique. Dans la vraie vie :
- Si vous écrivez dans
uploadset que votre serveur exécute du PHP dans ce dossier (mauvaise config), l’étape suivante est souvent un dépôt de.phpou un contournement via une extension exotique. - Si vous réutilisez le même pattern pour sauvegarder un template, un JSON, ou pire un “snippet”, vous basculez vers du RCE (exécution de code) ou de l’élévation de privilèges.
Autre variante que je vois souvent : une action AJAX admin exposée à tout le monde, sans nonce, qui met à jour une option sensible.
<?php
// VULNÉRABLE : action AJAX accessible aux visiteurs (nopriv) + pas de nonce + pas de capability
add_action('wp_ajax_nopriv_bpcab_update_option', 'bpcab_update_option_vulnerable');
function bpcab_update_option_vulnerable() {
$key = $_POST['key'] ?? '';
$value = $_POST['value'] ?? '';
// DANGER : update_option sur clé arbitraire
update_option($key, $value);
wp_send_json_success(['updated' => $key]);
}
Si un attaquant peut modifier des options arbitraires, il peut souvent :
- activer l’inscription et changer le rôle par défaut (selon configuration),
- modifier des réglages de plugin de sécurité,
- changer des URLs de redirection,
- injecter des scripts via options consommées côté front (selon thème/page builder).
Code sécurisé — la bonne implémentation
On reprend la même fonctionnalité (sauvegarder du CSS) mais on la rend robuste pour WordPress 6.9.4 + PHP 8.1. Objectif : permissions, validation stricte, limites, écriture sûre, et chargement propre côté front.
Version sécurisée (REST) : permissions + validation + écriture contrôlée
<?php
/**
* Plugin Name: BPCAB Custom CSS (SÉCURISÉ)
* Description: Exemple pédagogique : endpoint REST sécurisé pour enregistrer un CSS.
*/
add_action('rest_api_init', function () {
register_rest_route('bpcab/v1', '/save-css', [
'methods' => 'POST',
'callback' => 'bpcab_save_css_secure',
// On exige une capacité cohérente : ici, gérer les options du site
'permission_callback' => function (WP_REST_Request $request) {
return current_user_can('manage_options');
},
'args' => [
'css' => [
'type' => 'string',
'required' => true,
// Validation : limite de taille + type
'validate_callback' => function ($param) {
if (!is_string($param)) {
return false;
}
// 50 Ko max : ajustez selon besoin
return strlen($param) <= 50 * 1024;
},
// Sanitation : on nettoie au minimum (on garde du CSS valide)
'sanitize_callback' => 'bpcab_sanitize_css',
],
],
]);
});
/**
* Sanitation CSS : on supprime les balises et caractères de contrôle.
* Note : c'est volontairement conservateur, sans casser la majorité des CSS.
*/
function bpcab_sanitize_css(string $css): string {
// Retire tout ce qui ressemble à du HTML
$css = wp_strip_all_tags($css);
// Retire les caractères de contrôle (hors tab/newline)
$css = preg_replace('/[^P{C}tnr]+/u', '', $css);
// Normalise les fins de ligne
$css = str_replace(["rn", "r"], "n", $css);
return trim($css);
}
function bpcab_save_css_secure(WP_REST_Request $request): WP_REST_Response {
// Défense en profondeur : même si permission_callback est en place
if (!current_user_can('manage_options')) {
return new WP_REST_Response(['error' => 'forbidden'], 403);
}
$css = (string) $request->get_param('css');
// Écriture dans un sous-dossier dédié dans uploads
$upload_dir = wp_upload_dir();
if (!empty($upload_dir['error'])) {
return new WP_REST_Response(['error' => 'upload_dir_unavailable'], 500);
}
$dir = trailingslashit($upload_dir['basedir']) . 'bpcab-assets/';
if (!wp_mkdir_p($dir)) {
return new WP_REST_Response(['error' => 'mkdir_failed'], 500);
}
// Nom non prévisible + stable via hash du contenu (cache friendly)
$hash = substr(hash('sha256', $css), 0, 16);
$filename = "custom-{$hash}.css";
$target = $dir . $filename;
// Écriture atomique : fichier temporaire + rename
$tmp = $target . '.tmp';
$bytes = file_put_contents($tmp, $css, LOCK_EX);
if ($bytes === false) {
@unlink($tmp);
return new WP_REST_Response(['error' => 'write_failed'], 500);
}
if (!@rename($tmp, $target)) {
@unlink($tmp);
return new WP_REST_Response(['error' => 'rename_failed'], 500);
}
// Permissions raisonnables
@chmod($target, 0644);
// On stocke la référence en option (clé fixe !)
update_option('bpcab_custom_css_file', $filename, false);
return new WP_REST_Response([
'ok' => true,
'file' => $filename,
'bytes' => $bytes,
], 200);
}
Explication simple (ce qui change vraiment)
- Plus d’accès anonyme : seuls les admins (ou rôles avec
manage_options) peuvent appeler l’endpoint. - Entrée bornée : vous limitez la taille. Ça coupe net une classe de DoS “disque/mémoire”.
- Nom de fichier non prévisible : vous évitez les collisions et certains scénarios de remplacement.
- Écriture atomique : moins de fichiers partiellement écrits (problème réel quand cache/proxy martèle l’endpoint).
- Option à clé fixe : pas de
update_option($key)arbitraire.
Explication technique (les choix et les pièges)
Permission REST : la doc officielle insiste sur permission_callback (ne jamais laisser __return_true sur une route qui modifie l’état). Référence : Routes and Endpoints (REST API).
Nonce REST : si votre endpoint est appelé depuis wp-admin (ou depuis le front pour un utilisateur connecté), vous pouvez ajouter un nonce REST (X-WP-Nonce) côté JS. Ça protège surtout contre des scénarios CSRF via navigateur. Je ne le mets pas ici pour garder l’exemple PHP centré, mais en production je l’ajoute souvent.
Sanitation CSS : il n’existe pas de “sanitize_css” parfait dans le core. Votre stratégie doit être pragmatique : supprimer HTML, contrôler la taille, et éviter d’injecter ce CSS dans un contexte HTML non échappé. Le vrai risque vient souvent d’un mauvais contexte d’échappement (ex : imprimer dans un attribut).
Chargement du fichier CSS côté front (propre et cache-friendly)
Évitez d’echo du CSS en dur dans le header. En 2026, je privilégie un fichier statique + wp_enqueue_style, avec version basée sur le nom hashé.
<?php
add_action('wp_enqueue_scripts', function () {
$filename = get_option('bpcab_custom_css_file');
if (!$filename) {
return;
}
$upload_dir = wp_upload_dir();
$baseurl = trailingslashit($upload_dir['baseurl']) . 'bpcab-assets/';
$url = $baseurl . rawurlencode($filename);
// Version = hash inclus dans le nom, donc on peut mettre une version stable
wp_enqueue_style(
'bpcab-custom-css',
$url,
[],
null
);
}, 20);
Compatibilité Divi 5 / Elementor / Avada : où ce code casse souvent
Sur des sites builder, l’erreur classique est de générer ce CSS à chaque chargement de page (ou à chaque preview). Résultat : charge serveur, fichiers multiples, et parfois des race conditions.
- Divi 5 : déclenchez la génération à l’enregistrement d’un réglage (admin), pas en front. Si vous passez par un module custom, gardez l’écriture côté admin et ne laissez pas un endpoint public “pour la preview”.
- Elementor : même logique. Elementor a déjà ses mécanismes de CSS généré ; évitez de dupliquer. Si vous devez ajouter du CSS, utilisez un handle unique et ne surchargez pas le pipeline de génération.
- Avada : attention aux systèmes de cache/compilation. Après changement, il faut souvent purger le cache Avada et/ou un plugin de cache, sinon vous croyez que “ça ne marche pas” et vous réessayez… en multipliant les écritures.
Configuration serveur
Quand une alerte “actively exploited” sort, je durcis toujours côté serveur en parallèle du patch applicatif. Ça ne remplace pas la correction, mais ça réduit la fenêtre d’impact si un plugin tiers a un trou.
Bloquer l’exécution PHP dans wp-content/uploads (Apache)
Copiez ceci dans wp-content/uploads/.htaccess (si vous êtes sur Apache avec AllowOverride actif). C’est une des protections les plus rentables que vous puissiez déployer.
# À placer dans wp-content/uploads/.htaccess
<FilesMatch ".php$">
Require all denied
</FilesMatch>
# Bloque aussi quelques extensions souvent utilisées en contournement
<FilesMatch ".(phtml|phar|php[0-9]?|pht)$">
Require all denied
</FilesMatch>
Équivalent Nginx (recommandé si vous pouvez toucher la conf)
# Dans le server {} du vhost
location ~* ^/wp-content/uploads/.*.(php|phtml|phar|php[0-9]?|pht)$ {
deny all;
return 403;
}
Désactiver l’édition de fichiers depuis l’admin
Ça ne bloque pas une RCE, mais ça limite les dégâts si un attaquant obtient un accès admin.
<?php
// wp-config.php
define('DISALLOW_FILE_EDIT', true);
// Optionnel : bloque aussi l'installation/édition de plugins/thèmes depuis l'admin
// Attention : à activer si votre process de déploiement est propre (Git/CI)
define('DISALLOW_FILE_MODS', true);
Référence : Editing wp-config.php.
Forcer HTTPS et cookies plus sûrs
Si votre site est derrière un proxy/CDN, vérifiez que WordPress “voit” bien HTTPS, sinon vous aurez des cookies moins stricts.
<?php
// wp-config.php (exemple derrière proxy)
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
Headers HTTP (Apache)
Ces en-têtes ne “patchent” pas un plugin vulnérable, mais ils réduisent certains impacts (clickjacking, MIME sniffing) et améliorent l’hygiène globale.
# .htaccess à la racine (si mod_headers est actif)
<IfModule mod_headers.c>
Header set X-Content-Type-Options "nosniff"
Header set Referrer-Policy "strict-origin-when-cross-origin"
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
Header always set X-Frame-Options "SAMEORIGIN"
</IfModule>
Pour une CSP, je reste prudent : une CSP trop agressive casse vite Divi/Elementor/Avada et des plugins marketing. Si vous la déployez, faites-le en mode report-only d’abord.
Vérifier si votre site est vulnérable
Vous cherchez deux choses : (1) une surface d’attaque (endpoints suspects, plugins à risque), (2) des indices d’exploitation (logs, fichiers, comptes).
Audit rapide des plugins et versions (WP-CLI)
Sur un serveur avec WP-CLI, commencez par sortir l’inventaire. Référence WP-CLI : WP-CLI plugin command.
# Liste des plugins + statut
wp plugin list --fields=name,status,version,update,auto_update
# Liste des thèmes
wp theme list --fields=name,status,version,update,auto_update
# Vérifie la version WordPress
wp core version
Ce que je fais ensuite, très concrètement : je supprime (pas seulement désactive) tout plugin non utilisé. Les bots exploitent des plugins désactivés s’ils sont vulnérables via des fichiers accessibles directement.
Chercher des routes REST “ouvertes”
Sans scanner externe, vous pouvez déjà inspecter les routes exposées :
# Affiche l'index REST (peut être volumineux)
curl -s https://votre-domaine.tld/wp-json/ | head -n 80
Ce que vous cherchez :
- des namespaces inconnus (plugins “maison” ou obscurs),
- des routes qui modifient l’état (POST/PUT/DELETE),
- des endpoints qui parlent de import, template, settings, upload, sync.
Requêtes SQL de diagnostic (comptes admins suspects)
Si vous avez accès à la base (et une sauvegarde), cherchez les admins récemment créés. WordPress stocke les rôles dans wp_usermeta (préfixe variable). Exemple générique :
# Remplacez wp_ par votre préfixe si besoin
wp db query "
SELECT u.ID, u.user_login, u.user_email, u.user_registered
FROM wp_users u
ORDER BY u.user_registered DESC
LIMIT 20;
"
Puis vérifiez les rôles :
wp db query "
SELECT um.user_id, um.meta_value
FROM wp_usermeta um
WHERE um.meta_key = 'wp_capabilities'
ORDER BY um.user_id DESC
LIMIT 20;
"
Indicateur fréquent : un admin avec un email jetable, ou un login qui ressemble à une chaîne aléatoire.
Fichiers suspects (uploads, mu-plugins, wp-content)
Les webshells finissent souvent dans wp-content/uploads ou wp-content/mu-plugins. Cherchez des .php là où il ne devrait pas y en avoir.
# PHP dans uploads (ne devrait idéalement rien retourner)
find wp-content/uploads -type f ( -name "*.php" -o -name "*.phtml" -o -name "*.phar" ) -print
# mu-plugins (souvent utilisé pour persister)
ls -la wp-content/mu-plugins
# Fichiers récemment modifiés dans wp-content (à ajuster)
find wp-content -type f -mtime -7 -print | head -n 200
Analyse des logs : les patterns que je vois en incident
- pics sur
/wp-admin/admin-ajax.phpavec desaction=variés, - requêtes POST sur
/wp-json/depuis des IPs multiples, - tentatives directes sur
/wp-content/uploads/*.php, - user-agents “vides” ou très génériques,
- beaucoup de 200/302 sur des URLs qui n’existent pas normalement.
Tableau de diagnostic (symptômes réels)
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Redirections vers des sites tiers (souvent mobile uniquement) | JS injecté (thème, plugin, option), ou fichier modifié | Comparer fichiers thème/plugin avec versions officielles, inspecter wp_options (valeurs longues), vérifier header.php/footer.php |
Remplacer thèmes/plugins depuis sources officielles, nettoyer options injectées, purger caches CDN/plugin |
| Nouvel admin inconnu | Endpoint REST/AJAX sans permission, creds compromis | Audit des routes REST, actions AJAX, logs de connexion, liste des users | Supprimer l’utilisateur, révoquer sessions, corriger endpoint, changer secrets |
Fichiers .php dans uploads |
Upload non filtré ou exécution PHP autorisée dans uploads | find + accès HTTP direct au fichier |
Supprimer fichiers, bloquer exécution PHP via Nginx/Apache, patcher plugin d’upload |
| Charge CPU anormale, beaucoup de POST | Brute force, abus d’admin-ajax, endpoint public coûteux | Logs + top endpoints, APM si dispo | Rate limiting (WAF/CDN), corriger endpoint, cache, désactiver fonctionnalités exposées |
Erreurs de sécurité fréquentes
| Erreur | Risque | Solution |
|---|---|---|
permission_callback => '__return_true' sur une route REST qui écrit/modifie |
Modification non authentifiée (prise de contrôle) | Exiger une capability (current_user_can) et, si contexte navigateur, nonce REST |
AJAX wp_ajax_nopriv_* pour une action sensible |
Appel anonyme (bots) + abus massif | N’utiliser nopriv que pour des actions réellement publiques et idempotentes |
update_option($_POST['key'], ...) (clé arbitraire) |
Altération de configuration, injections indirectes | Clé fixe + whitelist stricte si plusieurs options |
Oublier check_ajax_referer() / nonces |
CSRF (attaque via navigateur d’un admin connecté) | Nonce + capability ; ne confondez pas auth et anti-CSRF |
| Copier le code au mauvais endroit (functions.php du thème parent) | Perte au prochain update, comportements imprévisibles | Plugin dédié ou thème enfant, idéalement versionné (Git) |
| Tester sur production sans sauvegarde | Indisponibilité, perte de données | Staging + sauvegarde + plan de rollback |
| Hook inadapté / mauvaise priorité (REST init trop tard, enqueue au mauvais moment) | Endpoint non protégé comme prévu, assets non chargés | Respecter rest_api_init, wp_enqueue_scripts, priorités explicites |
| Snippet cassé (parenthèse/point-virgule) via plugin de snippets | White screen, site down | Déployer via fichier plugin, lint, logs PHP, staging |
| PHP trop ancien (ou “juste” 8.0) sur un site moderne | Plugins non patchés, incompatibilités, surface d’attaque | PHP 8.1+ (idéalement plus récent si votre stack le permet), monitoring erreurs |
| Oublier de purger cache (plugin/CDN/navigateur) | Vous croyez que le patch ne fonctionne pas, vous réouvrez des accès | Purge ciblée, invalider assets, vérifier headers cache |
| Permaliens non régénérés après changements structurels | Routes inattendues, 404, contournements via anciennes URLs | Réenregistrer les permaliens (ou wp rewrite flush avec précaution) |
Checklist de durcissement
- Core : WordPress 6.9.4 à jour, mises à jour automatiques activées quand possible.
- Plugins/thèmes : supprimer ceux inutilisés, activer l’auto-update pour les plugins critiques (ou process CI/CD).
- Comptes : 2FA pour admins, mots de passe uniques, supprimer les comptes inactifs, limiter le nombre d’admins.
- REST/AJAX : audit des endpoints custom ; chaque action qui modifie doit avoir capability + validation + limites.
- Uploads : interdire exécution PHP, contrôler types MIME, surveiller fichiers récents.
- wp-config.php :
DISALLOW_FILE_EDIT(et éventuellementDISALLOW_FILE_MODS), clés de sécurité à jour. - Headers :
nosniff, policy de referrer, protections framing. - Backups : sauvegardes testées (restauration validée), rétention, stockage hors serveur.
- Logs : accès + erreurs PHP centralisés, alertes sur pics
admin-ajax.php/wp-json. - WAF/CDN : rate limiting sur endpoints sensibles, blocage géographique si pertinent, règles gérées.
Que faire si le site est déjà compromis ?
Si vous suspectez une compromission, l’objectif n°1 est d’arrêter l’hémorragie sans détruire les preuves. Le piège classique : “je supprime deux fichiers et c’est bon”. Non. La persistance est souvent ailleurs.
- Isolez : mettez le site en maintenance (ou bloquez temporairement via WAF/CDN), coupez les accès inutiles (FTP partagés, anciens comptes).
- Sauvegardez pour forensique : export DB + archive complète des fichiers avant nettoyage. Stockez hors serveur. Ça vous sauvera si vous devez comprendre le point d’entrée.
- Identifiez le point d’entrée : plugin récemment mis à jour ? endpoint custom ? compte admin créé ? Dans mon expérience, si vous ne trouvez pas le point d’entrée, vous rechuterez.
- Remplacez le core : réinstallez WordPress depuis une source officielle (même version) plutôt que “nettoyer fichier par fichier”. Avec WP-CLI :
# Réinstalle les fichiers du core (ne touche pas wp-content) wp core download --force - Remplacez plugins et thèmes : supprimez et réinstallez depuis WordPress.org/plugins ou les sources éditeurs. Évitez les ZIP “trouvés” dans un vieux dossier.
- Inspectez et nettoyez wp-content : surtout
uploads,mu-plugins,wp-content/plugins(fichiers ajoutés),wp-content/cache(parfois utilisé pour cacher du code). - Révoquez toutes les sessions : forcez la déconnexion globale (plugins de sécurité ou rotation de clés). Changez tous les mots de passe (WP, DB, FTP/SSH, panel, API).
- Rotation des secrets : régénérez les clés/salts WordPress. Référence : Security keys (wp-config.php).
- Vérifiez la base : cherchez du code injecté dans
wp_options(options autoload énormes), des users inconnus, des tâches cron suspectes. - Patch + durcissement serveur : bloquez PHP dans uploads, ajoutez rate limiting, corrigez les endpoints.
- Re-scan : seulement après nettoyage + durcissement, relancez un scan (outil de sécurité, vérif manuelle, comparaison de fichiers).
Si vous avez des données personnelles (formulaires, comptes), traitez l’incident comme une fuite potentielle. Ça implique parfois une notification selon votre contexte légal. Ne minimisez pas.
Conseils de maintenance et compatibilité
Mettre à jour sans casser Divi/Elementor/Avada
Le conflit classique : un plugin de cache/minification garde des assets anciens, et vous croyez que le patch n’a rien changé. Sur des stacks builder :
- Purger le cache du plugin (WP Rocket/LiteSpeed/etc.), puis le cache du builder (quand il existe), puis le CDN.
- Si vous générez des fichiers (CSS/JS), versionnez-les par hash (comme dans l’exemple) pour éviter les soucis de cache.
Performance : limiter l’exposition des endpoints coûteux
Un endpoint public “qui fait un truc lourd” (générer un PDF, recalculer un cache, importer…) devient une cible DoS même sans faille. Ajoutez :
- rate limiting côté WAF/CDN,
- limites de taille sur payloads,
- timeouts raisonnables,
- mise en queue (Action Scheduler / cron) si c’est long.
Anti-patterns que je refuse en audit
- Stocker du HTML/JS “libre” dans une option et l’imprimer sans contexte d’échappement.
- Utiliser
eval()ou des “snippets exécutables” via l’admin (même si “c’est pratique”). - Créer des endpoints REST sans tests de permission, “parce que c’est derrière Cloudflare”. Un WAF n’est pas une permission applicative.
Compatibilité future WordPress 6.9.4+
Restez sur les APIs publiques (REST, Settings API, enqueue), et évitez les accès directs à la DB “par habitude”. Quand vous devez requêter, utilisez $wpdb proprement et préparez vos requêtes. Doc : Database API.
Ressources
- Hardening WordPress (documentation officielle)
- REST API : Routes and Endpoints
- Nonces (Security API)
- current_user_can() (référence)
- register_rest_route() (référence)
- wp-config.php (référence développeurs)
- Dépôt GitHub WordPress (miroir)
- WordPress Core Trac
- PHP Manual : Security
FAQ
“Attackers actively exploiting critical” veut dire que WordPress est cassé ?
Le plus souvent, non. Ça veut dire qu’une vulnérabilité critique (souvent plugin/thème) est suffisamment facile à automatiser pour être exploitée en masse. Le core WordPress à jour est rarement le point d’entrée principal sur les incidents que je traite.
Si je désactive un plugin vulnérable, je suis protégé ?
Pas toujours. Si la faille est exploitable via un fichier accessible directement (ou si des endpoints restent exposés), le code peut rester atteignable. Supprimez le plugin si vous ne l’utilisez pas.
Dois-je désactiver la REST API pour me protéger ?
Je ne le recommande pas sur un site moderne : Gutenberg et beaucoup de plugins en dépendent. Le bon réflexe est de sécuriser vos routes custom (permissions + validation) et de durcir les endpoints sensibles.
Les nonces suffisent-ils ?
Non. Un nonce protège surtout contre le CSRF (attaque via navigateur). Pour empêcher un utilisateur non autorisé, il faut une capability (ex : manage_options) et un contrôle côté serveur.
Pourquoi bloquer PHP dans uploads est si efficace ?
Parce que beaucoup de chaînes d’attaque finissent par déposer un fichier exécutable dans un dossier d’upload. Si le serveur refuse d’exécuter du PHP là, vous cassez une étape critique de persistance.
Quels dossiers sont les plus visés ?
wp-content/uploads, wp-content/mu-plugins, parfois wp-includes si l’attaquant a déjà une écriture large. Les caches (wp-content/cache) sont aussi utilisés pour se cacher.
Je vois beaucoup de requêtes sur admin-ajax.php, c’est forcément une attaque ?
Pas forcément : certains plugins (stats, formulaires, builders) l’utilisent beaucoup. Ce qui doit vous alerter : actions inconnues, erreurs répétées, pics soudains, et corrélation avec des lenteurs.
Comment éviter les snippets “dangereux” copiés du web ?
Traitez-les comme du code de prod : staging, revue, tests, et surtout vérifiez qu’ils utilisent les APIs modernes (REST/AJAX avec permissions, nonces, sanitation). Beaucoup de vieux snippets sont incompatibles avec les pratiques actuelles et parfois franchement risqués.
Qu’est-ce qui casse le plus souvent après un durcissement (headers/CSP) ?
Une CSP trop stricte casse fréquemment Divi/Elementor/Avada et les scripts marketing. Déployez progressivement, commencez en report-only, et gardez une liste d’origines autorisées minimale.
Est-ce que DISALLOW_FILE_MODS est une bonne idée ?
Oui si vous avez un process de déploiement propre (CI/CD, Git, accès SSH maîtrisé). Sinon, vous allez finir par le désactiver “temporairement” en urgence, et c’est là que les erreurs arrivent.
Que dois-je surveiller en continu ?
Au minimum : création/modification d’admins, modifications de fichiers dans wp-content, pics de trafic sur wp-json/admin-ajax.php, et taille anormale de certaines options autoload. C’est souvent là que les incidents se révèlent en premier.