Si vous avez déjà vu passer dans vos logs une requête vers /wp-content/uploads/2024/11/cache.php (ou pire, un ?cmd= sur un fichier qui n’a rien à faire là), vous avez probablement frôlé — ou subi — une backdoor PHP.

Ce guide cible WordPress 6.9.4 (avril 2026) et PHP 8.1+. Je pars du principe que vous savez utiliser SSH, WP‑CLI, lire un log Nginx/Apache, et que vous avez déjà mis les mains dans un mu-plugin.

La menace

Une backdoor PHP sur WordPress n’est pas “juste” un fichier suspect. C’est un point d’accès persistant qui permet à un attaquant de revenir quand il veut, même après un changement de mot de passe admin. Dans mon expérience, les backdoors les plus pénibles ne se voient pas : elles se cachent dans uploads/, dans un plugin “nulled”, ou dans un wp-config.php modifié avec une seule ligne qui semble innocente.

Concrètement, si une backdoor est en place, l’attaquant peut :

  • Exécuter du code à distance (RCE) : lancer des commandes système, télécharger d’autres malwares, pivoter vers d’autres sites du même serveur.
  • Créer des comptes administrateurs furtifs ou réactiver des comptes supprimés.
  • Voler des données : cookies, tokens de sessions, clés d’API, exports WooCommerce, emails, formulaires.
  • Injecter du spam SEO (cloaking, pages satellites) sans toucher vos contenus visibles dans l’admin.
  • Monétiser votre trafic via redirections conditionnelles (uniquement mobile, uniquement Googlebot, uniquement certaines géos).

La fréquence réelle est difficile à chiffrer globalement sans tomber dans le marketing d’éditeurs de sécurité. Ce que je peux vous dire de façon fiable : sur des hébergements mutualisés et des sites qui installent des thèmes/plugins non officiels, j’ai vu des backdoors presque systématiquement après une compromission initiale. Et sur des sites Elementor/Divi/Avada, le “facteur aggravant” n’est pas le builder en lui-même, mais l’écosystème : beaucoup d’addons tiers, parfois abandonnés, parfois téléchargés hors des canaux officiels.

Risque en langage simple : une backdoor, c’est une porte dérobée cachée dans votre code. Vous pouvez repeindre la porte d’entrée (changer les mots de passe), l’attaquant passera par la porte cachée tant qu’elle existe.

Résumé rapide

  • Ne cherchez pas “un virus” : cherchez des points d’exécution (fichiers PHP inattendus, hooks injectés, options WP modifiées, tâches cron suspectes).
  • Commencez par l’inventaire : fichiers récemment modifiés, plugins inconnus, mu-plugins, tâches WP‑Cron, utilisateurs admins.
  • Traitez la cause : plugin vulnérable, identifiants FTP compromis, clé SSH volée, accès au panneau d’hébergement, etc.
  • Durcissez l’exécution PHP : interdisez PHP dans uploads/, bloquez l’accès à .git, limitez l’éditeur de fichiers, ajoutez des headers.
  • Faites une restauration propre : un “nettoyage à la main” sans baseline (core + plugins) laisse souvent une persistance.
  • Automatisez la détection : WP‑CLI + scans de patterns + contrôle d’intégrité (checksums) + monitoring de modifications.

Code vulnérable — ce qu’il ne faut PAS faire

Les backdoors “classiques” s’appuient sur une idée simple : obtenir un endroit où PHP peut exécuter une entrée contrôlée par l’utilisateur. Le vecteur le plus courant sur WordPress reste un plugin/thème qui accepte un paramètre et l’exécute (directement ou indirectement), ou un upload de fichier mal filtré.

Exemple réaliste : endpoint AJAX qui exécute une “commande de diagnostic”

J’ai souvent vu ce snippet (ou une variante) dans des plugins maison, des snippets collés dans functions.php, ou des “must-use” bricolés. L’intention est “pratique”. L’effet est catastrophique.

<?php
/**
 * DANGEREUX : exemple de code vulnérable.
 * Ne déployez jamais ça sur un site public.
 */

add_action('wp_ajax_nopriv_site_debug', 'site_debug_vulnerable');
add_action('wp_ajax_site_debug', 'site_debug_vulnerable');

function site_debug_vulnerable() {
	// Erreur : aucune vérification de permission, aucun nonce
	$cmd = isset($_REQUEST['cmd']) ? $_REQUEST['cmd'] : '';

	// Erreur : exécution directe d'une entrée utilisateur
	$output = shell_exec($cmd);

	// Erreur : fuite d'information + pas de format JSON strict
	echo $output;
	wp_die();
}

Pourquoi c’est exploitable (sans “mode d’emploi” d’attaque)

Voici ce qui se passe en coulisses :

  • wp_ajax_nopriv_* expose l’action aux visiteurs non authentifiés. Un bot n’a même pas besoin d’un compte.
  • $_REQUEST mélange GET/POST/COOKIE : vous élargissez la surface d’attaque inutilement.
  • shell_exec() exécute une commande système. À ce stade, vous n’êtes plus “dans WordPress”, vous êtes dans l’OS.
  • La réponse brute renvoie le résultat. Même si l’attaquant n’obtient pas une exécution complète, il obtient souvent des informations (chemins, utilisateurs système, etc.).

Variante tout aussi dangereuse : “évaluer” du PHP depuis une option

Autre pattern fréquent : un plugin stocke du code dans une option (ou dans un champ ACF) et l’exécute pour “personnaliser”. Ça finit souvent en persistance après compromission.

<?php
// DANGEREUX : exécuter du code arbitraire stocké en base
add_action('init', function () {
	$payload = get_option('custom_php_snippet');
	if (!empty($payload)) {
		// Erreur : eval = exécution arbitraire
		eval($payload);
	}
});

Une fois qu’un attaquant peut écrire dans la base (SQLi, compte admin volé, plugin vulnérable), il n’a plus besoin d’écrire un fichier : il injecte un payload dans l’option et le code s’exécute à chaque page.

Code sécurisé — la bonne implémentation

Je reprends la même “fonctionnalité” (diagnostic), mais avec une approche qui tient en production : pas d’exécution de commandes, pas d’accès public, permissions strictes, nonce, liste blanche, et sortie JSON.

Objectif : exposer un diagnostic WordPress sans RCE

Le bon compromis, c’est d’exposer des informations déjà disponibles via WordPress (versions, état de cache, santé du site) et de refuser tout ce qui ressemble à une commande. Si vous avez réellement besoin de commandes OS, faites-le via SSH, pas via HTTP.

<?php
/**
 * Exemple sécurisé (WordPress 6.9.4+, PHP 8.1+).
 * À placer dans un plugin dédié ou un mu-plugin, pas dans functions.php.
 */

add_action('wp_ajax_site_debug', 'site_debug_secure');

function site_debug_secure() : void {
	// 1) Authentification : pas de nopriv
	if (!is_user_logged_in()) {
		wp_send_json_error(['message' => 'Non authentifié.'], 401);
	}

	// 2) Autorisation : capability explicite
	// manage_options est souvent trop large, mais acceptable pour un endpoint admin.
	// Sur un site d'agence, je préfère une capability dédiée via un rôle.
	if (!current_user_can('manage_options')) {
		wp_send_json_error(['message' => 'Accès refusé.'], 403);
	}

	// 3) Anti-CSRF : nonce obligatoire
	$nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
	if (!wp_verify_nonce($nonce, 'site_debug_secure')) {
		wp_send_json_error(['message' => 'Nonce invalide.'], 403);
	}

	// 4) Entrée : liste blanche d'actions, pas de "cmd"
	$action = isset($_POST['action_name']) ? sanitize_key(wp_unslash($_POST['action_name'])) : '';
	$allowed = [
		'wp_versions',
		'active_plugins',
		'health_summary',
	];

	if (!in_array($action, $allowed, true)) {
		wp_send_json_error(['message' => 'Action non autorisée.'], 400);
	}

	// 5) Exécution : uniquement des appels WordPress, pas d'OS
	$data = match ($action) {
		'wp_versions' => [
			'wp'  => get_bloginfo('version'),
			'php' => PHP_VERSION,
		],
		'active_plugins' => [
			'plugins' => array_values((array) get_option('active_plugins', [])),
		],
		'health_summary' => site_debug_health_summary(),
		default => [],
	};

	// 6) Sortie : JSON strict + status code correct
	wp_send_json_success($data, 200);
}

/**
 * Résumé minimal de "Site Health" sans exposer de secrets.
 * Évitez d'exfiltrer des chemins absolus, des clés, ou des contenus de logs.
 */
function site_debug_health_summary() : array {
	$summary = [
		'has_https' => is_ssl(),
		'wp_debug'  => (bool) (defined('WP_DEBUG') && WP_DEBUG),
	];

	// Note : la logique Site Health complète est plus riche.
	// Ici on reste volontairement minimal pour limiter la fuite d'infos.
	return $summary;
}

Explication simple

  • Vous supprimez l’accès public (nopriv).
  • Vous imposez une permission admin.
  • Vous bloquez le CSRF avec un nonce.
  • Vous remplacez “commande libre” par une liste blanche d’actions.
  • Vous renvoyez du JSON propre, pas du texte brut.

Explication technique (les détails qui évitent les surprises)

  • sanitize_key() sur un nom d’action : réduit l’espace de caractères autorisés (utile contre les injections “créatives”).
  • wp_unslash() avant sanitization : WordPress “slashe” encore certaines entrées. Oublier wp_unslash() donne des bugs intermittents.
  • match (PHP 8.1+) : lisible, et évite les fallthrough de switch.
  • Pas d’info sensible : un endpoint de debug devient vite un endpoint d’exfiltration. Gardez-le minimal.

Pattern avancé : isoler la logique dans un service (DI légère)

Sur des sites complexes (Divi 5 + Woo + multisite), je préfère éviter les fonctions globales et encapsuler dans une classe, chargée via un mini-container. Vous gagnez en testabilité et en lisibilité lors d’un incident.

<?php
/**
 * Mini "container" ultra simple pour WordPress.
 * Objectif : centraliser l'enregistrement des hooks et les dépendances.
 */

final class App_Container {
	private array $services = [];

	public function set(string $id, callable $factory) : void {
		$this->services[$id] = $factory;
	}

	public function get(string $id) : object {
		if (!isset($this->services[$id])) {
			throw new RuntimeException("Service introuvable : {$id}");
		}
		$service = ($this->services[$id])($this);
		if (!is_object($service)) {
			throw new RuntimeException("Service invalide : {$id}");
		}
		return $service;
	}
}

final class Debug_Controller {
	public function hooks() : void {
		add_action('wp_ajax_site_debug', [$this, 'handle']);
	}

	public function handle() : void {
		if (!is_user_logged_in() || !current_user_can('manage_options')) {
			wp_send_json_error(['message' => 'Accès refusé.'], 403);
		}

		$nonce = isset($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : '';
		if (!wp_verify_nonce($nonce, 'site_debug_secure')) {
			wp_send_json_error(['message' => 'Nonce invalide.'], 403);
		}

		$action = isset($_POST['action_name']) ? sanitize_key(wp_unslash($_POST['action_name'])) : '';
		$allowed = ['wp_versions', 'active_plugins'];

		if (!in_array($action, $allowed, true)) {
			wp_send_json_error(['message' => 'Action non autorisée.'], 400);
		}

		$data = ($action === 'wp_versions')
			? ['wp' => get_bloginfo('version'), 'php' => PHP_VERSION]
			: ['plugins' => array_values((array) get_option('active_plugins', []))];

		wp_send_json_success($data, 200);
	}
}

// Bootstrap (mu-plugin ou plugin)
add_action('plugins_loaded', function () {
	$container = new App_Container();
	$container->set('debug_controller', fn() => new Debug_Controller());
	$container->get('debug_controller')->hooks();
}, 1);

Piège réel : coller ce code dans un plugin de snippets qui charge trop tard (ou dans un thème enfant) et oublier la priorité. Si votre hook arrive après une logique qui meurt tôt, vous “croyez” avoir protégé un endpoint… alors qu’un autre endpoint vulnérable reste actif.

Configuration serveur

La détection ne suffit pas : une bonne partie des backdoors se contentent d’un fichier PHP déposé dans un répertoire web accessible. Le durcissement serveur coupe net cette classe d’attaque.

Bloquer l’exécution PHP dans wp-content/uploads (Apache)

Sur Apache avec .htaccess, c’est un classique. À placer dans wp-content/uploads/.htaccess.

# Créez/éditez wp-content/uploads/.htaccess
# (Ce bloc est du .htaccess ; classé en PHP ici uniquement pour le format imposé)
<IfModule mod_php.c>
  php_flag engine off
</IfModule>

<IfModule mod_php7.c>
  php_flag engine off
</IfModule>

<FilesMatch ".(php|phtml|phar)$">
  Require all denied
</FilesMatch>

Edge case : selon la configuration (PHP-FPM, proxy_fcgi), php_flag peut être ignoré. Le FilesMatch reste utile, mais le plus fiable est une règle au niveau du vhost.

Bloquer l’exécution PHP dans uploads (Nginx)

À mettre dans le server block du site. Adaptez le chemin racine.

# /etc/nginx/sites-available/votre-site.conf
location ^~ /wp-content/uploads/ {
  location ~ .(php|phtml|phar)$ {
    deny all;
    return 403;
  }
}

Piège réel : vous ajoutez la règle, mais votre CDN (ou un cache type Varnish) continue de servir une ancienne version. Purgez le cache et retestez.

Verrouiller wp-config.php (réduction de surface)

Dans wp-config.php, vous pouvez limiter certains vecteurs. Attention : ces réglages ne “nettoient” rien, ils réduisent l’impact.

<?php
// Désactive l'éditeur de fichiers (évite une escalade via admin compromis)
define('DISALLOW_FILE_EDIT', true);

// Optionnel : bloque l'installation/mise à jour via l'admin (à réserver aux workflows CI/CD)
define('DISALLOW_FILE_MODS', false);

// Réduit les fuites d'erreurs PHP en prod
define('WP_DEBUG', false);
define('WP_DEBUG_DISPLAY', false);

Si vous gérez des sites clients : DISALLOW_FILE_EDIT m’a évité plusieurs incidents où un compte admin volé injectait une backdoor via l’éditeur de thème.

Headers HTTP utiles (sécurité navigateur)

Ces headers ne stoppent pas une backdoor PHP, mais ils réduisent les injections et les vols de session. Sur Apache :

# (Bloc .htaccess)
<IfModule mod_headers.c>
  Header always set X-Content-Type-Options "nosniff"
  Header always set X-Frame-Options "SAMEORIGIN"
  Header always set Referrer-Policy "strict-origin-when-cross-origin"
  Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>

Pour une CSP, évitez le copier-coller aveugle : Divi 5, Elementor et Avada injectent souvent du inline JS/CSS. Une CSP stricte casse vite le rendu si vous ne la construisez pas progressivement.

Vérifier si votre site est vulnérable

L’objectif n’est pas de “scanner au hasard”. Vous voulez une méthode reproductible : baseline (checksums), recherche de patterns, corrélation avec les logs, et inspection des points de persistance WordPress (options, cron, mu-plugins).

1) Vérifier l’intégrité du core WordPress (WP‑CLI)

Si vous avez WP‑CLI sur le serveur :

wp core verify-checksums --path=/chemin/vers/wordpress

Si ça remonte des fichiers modifiés dans wp-includes ou wp-admin, partez du principe que le core a été altéré. Dans ce cas, je ne “répare” pas fichier par fichier : je remplace le core par une copie propre de la même version (ou je mets à jour vers la dernière 6.9.x si possible).

2) Lister les fichiers PHP dans uploads (signal fort)

Un site WordPress standard n’a aucune raison d’avoir des .php dans wp-content/uploads.

find wp-content/uploads -type f ( -name "*.php" -o -name "*.phtml" -o -name "*.phar" ) -print

Edge case : certains plugins legacy déposent des “handlers” dans uploads (mauvaise pratique). Sur WordPress 6.9.4, considérez ça comme suspect et migrez.

3) Rechercher des patterns typiques (sans tomber dans le faux positif permanent)

Vous cherchez des indicateurs, pas une preuve unique. Combinez plusieurs patterns :

# Fonctions fréquemment utilisées dans des backdoors
grep -RIn --exclude-dir=node_modules --exclude-dir=vendor 
  -E "eval(|assert(|base64_decode(|gzinflate(|str_rot13(|shell_exec(|system(|passthru(|proc_open(" 
  wp-content

Attention : base64_decode() et gzinflate() existent dans du code légitime (licences, assets encodés, SDK). Ce qui m’alerte : usage combiné + chaînes très longues + exécution derrière (eval, assert) + obfuscation.

4) Inspecter les mu-plugins et drop-ins

Deux emplacements souvent oubliés :

  • wp-content/mu-plugins/ (chargés automatiquement)
  • wp-content/ avec des drop-ins : object-cache.php, advanced-cache.php, db.php
ls -la wp-content/mu-plugins
ls -la wp-content | egrep "object-cache.php|advanced-cache.php|db.php"

J’ai déjà vu des backdoors planquées dans object-cache.php sur des sites avec Redis “installé à la main”. Le fichier avait l’air normal, mais un include conditionnel pointait vers un fichier dans uploads.

5) Vérifier WP‑Cron (persistance silencieuse)

Une persistance propre utilise souvent WP‑Cron pour “réinstaller” la backdoor si vous la supprimez.

wp cron event list --fields=hook,next_run,recurrence --format=table

Cherchez :

  • des hooks inconnus avec des noms random
  • des récurrences très fréquentes
  • des événements qui appellent des fonctions “utilitaires” non documentées

6) Diagnostiquer via la base (options et utilisateurs)

Deux requêtes SQL simples (à exécuter via un client SQL, ou wp db query) :

wp db query "SELECT option_name, LENGTH(option_value) AS size FROM wp_options WHERE autoload='yes' ORDER BY size DESC LIMIT 20;"
wp db query "SELECT ID, user_login, user_email FROM wp_users ORDER BY ID DESC LIMIT 20;"

Ce que je cherche souvent :

  • une option énorme autoloadée (payload obfusqué, spam SEO, script injecté)
  • un admin récent “qui n’existe dans aucune procédure interne”

7) Lire les logs : le signal que les scans ne voient pas

Sur Nginx/Apache, cherchez des accès vers des chemins anormaux :

  • /wp-content/uploads/ suivi d’un fichier .php
  • des requêtes vers admin-ajax.php avec des actions inconnues
  • des 200 sur des fichiers qui devraient être statiques

Commande générique (adaptez le chemin du log) :

grep -E "uploads/.*.(php|phtml|phar)|admin-ajax.php" /var/log/nginx/access.log | tail -n 200

Tableau de diagnostic (symptômes courants)

Symptôme Cause probable Vérification Solution
Redirections SEO uniquement sur mobile Backdoor conditionnelle (UA/geo) dans un plugin ou wp_options Comparer HTML servi via curl desktop vs mobile, inspecter options volumineuses Restaurer depuis une sauvegarde propre + remplacer plugins + durcissement uploads
Fichiers PHP apparaissent dans uploads Upload non filtré + exécution PHP autorisée find wp-content/uploads -name "*.php" Bloquer PHP dans uploads + nettoyer + patcher plugin vulnérable
Des admins “fantômes” reviennent WP‑Cron ou mu-plugin réinjecte un user wp cron event list, inspecter mu-plugins Supprimer persistance + rotation des secrets + restaurer DB si nécessaire
CPU élevé / pics de processus PHP-FPM Spam, brute force, ou backdoor qui mine/scan Top, logs d’accès, requêtes répétées WAF/rate limit + nettoyage + blocage endpoints exposés
Emails sortants anormaux Injection via formulaire + backdoor mailer Logs SMTP, recherche mail( dans wp-content Corriger formulaire + SPF/DKIM + rotation mots de passe + nettoyage

Erreurs de sécurité fréquentes

Erreur Risque Solution
Copier du code de “cleaner” dans functions.php du thème Perte du correctif au changement de thème + exécution dans le mauvais contexte Créer un plugin dédié ou un mu-plugin versionné
Oublier un point-virgule / parenthèse dans un mu-plugin Fatal error, site down (et parfois contournement de protections) Tester sur staging, activer logs, déployer via CI
Utiliser wp_ajax_nopriv_* “pour tester” et oublier de le retirer Endpoint public exploitable Jamais de nopriv pour un outil admin, et contrôle de capability + nonce
Confondre actions et filtres (ex : add_filter au lieu de add_action) Le code de sécurité ne s’exécute pas, faux sentiment de protection Vérifier les hooks, ajouter des tests et logs temporaires
Tester directement en production sans sauvegarde Perte de données, downtime, restauration incomplète Snapshot + staging + plan de rollback
Oublier de vider le cache (plugin/CDN) Vous pensez avoir supprimé une redirection/backdoor, mais elle est servie en cache Purge cache plugin + CDN + navigateur, retester en curl
Utiliser un hook inadapté (trop tôt/trop tard) Protection non appliquée, race conditions Pour sécurité : plugins_loaded tôt, et éviter init si possible
Snippet cassé par un plugin de snippets Code tronqué/encodé, mise à jour qui supprime la protection Déployer sous forme de plugin versionné (Git)
Suivre un ancien tutoriel PHP 5/7 Incompatibilités PHP 8.1+, warnings, comportements inattendus Adapter au typage, aux APIs actuelles, tester sur PHP 8.1/8.2
Permaliens non régénérés après restauration 404, comportements bizarres, endpoints cachés qui restent accessibles Régénérer les permaliens et vérifier les règles serveur

Checklist de durcissement

  • Bloquez PHP dans wp-content/uploads (Nginx/Apache) et testez qu’un .php renvoie bien 403.
  • Vérifiez les checksums du core via WP‑CLI et remplacez tout fichier modifié.
  • Supprimez les plugins/thèmes non officiels (nulled) et remplacez par des versions depuis wordpress.org/plugins ou l’éditeur.
  • Inspectez mu-plugins et drop-ins (object-cache.php, advanced-cache.php).
  • Listez et auditez les admins (et révoquez ceux non justifiés).
  • Rotation des secrets : mots de passe WP, FTP/SFTP, DB, clés API, et AUTH_KEY/SALT dans wp-config.php.
  • Désactivez l’éditeur de fichiers (DISALLOW_FILE_EDIT).
  • Durcissez les permissions fichiers (pas de 777, propriétaire cohérent, pas d’écriture globale).
  • Activez un monitoring de modifications (hash, mtime) sur wp-content et alerting.
  • Mettre à jour WordPress 6.9.4+ et tout l’écosystème, puis supprimer ce qui n’est pas maintenu.

Que faire si le site est déjà compromis ?

Si vous avez un doute sérieux, évitez le “je supprime deux fichiers et c’est bon”. Une backdoor est souvent une persistance parmi plusieurs. Voici un plan d’action qui tient sur des incidents réels.

  1. Isoler : mettez le site en maintenance (ou restreignez par IP) le temps d’investiguer. Sur un e-commerce, faites-le hors heures de pointe si possible, mais ne traînez pas.
  2. Sauvegarder pour analyse : faites une copie complète fichiers + base. Cette sauvegarde n’est pas pour restaurer, elle est pour comprendre (et éventuellement fournir à l’hébergeur/assurance).
  3. Identifier le point d’entrée : plugin vulnérable, compte admin compromis, identifiants FTP, panel d’hébergement. Sans ça, vous nettoyez pour rien.
  4. Remplacer le core WordPress par une copie propre (même version ou mise à jour). Vérifiez avec wp core verify-checksums.
  5. Réinstaller plugins et thèmes depuis des sources sûres : supprimez le dossier, puis réinstallez. Ne “nettoyez” pas un plugin nulled : remplacez-le.
  6. Nettoyer wp-content/uploads : supprimez tout .php/.phtml/.phar, inspectez les fichiers récemment modifiés. Les backdoors se cachent aussi dans des .ico ou .jpg avec du PHP si le serveur interprète mal (rare, mais vu).
  7. Auditer la base :
    • supprimez les utilisateurs suspects
    • inspectez wp_options pour des options énormes ou inconnues
    • vérifiez les URLs du site (home/siteurl) et les redirections
  8. Rotation des secrets :
  9. Vérifier WP‑Cron et tâches système : supprimez les événements inconnus, vérifiez qu’aucun cron système ne réinjecte des fichiers.
  10. Durcir le serveur : bloquez PHP dans uploads, bloquez l’accès à .git, ajoutez rate limiting sur wp-login.php si pertinent.
  11. Contrôler après remise en ligne : surveillez logs + modifications de fichiers pendant 72h. Les réinfections arrivent vite si la cause persiste.

Piège que je vois souvent : restaurer une sauvegarde “qui marche” mais déjà compromise. Si vous n’avez pas de point de restauration fiable, partez sur une reconstruction (core + plugins propres) et réimportez uniquement le contenu (uploads filtrés + DB nettoyée).

Conseils de maintenance et compatibilité

Sur WordPress 6.9.4, la maintenance sécurité efficace ressemble à du DevOps léger : inventaire, mises à jour, contrôles d’intégrité, et réduction de surface.

Compatibilité Divi 5 / Elementor / Avada : où les backdoors se cachent vraiment

  • Addons tiers : c’est la zone rouge. Gardez une liste des addons, leur éditeur, et leur date de dernière mise à jour.
  • Templates importés : les builders importent du JSON/HTML. Une backdoor PHP ne vient pas “directement” d’un template, mais un template peut injecter du JS malveillant si vous autorisez des scripts.
  • Code snippets : beaucoup de sites builder utilisent un plugin de snippets. C’est pratique, mais c’est aussi une surface d’attaque (stockage en DB, exécution globale). Préférez un plugin maison versionné.

Performance/SEO : impacts d’un nettoyage

  • Après suppression de pages spam, prévoyez des 404 : gérez-les proprement (Search Console) et évitez les redirections massives vers la home.
  • Après durcissement (403 sur uploads), vérifiez que vos médias restent accessibles (images, webp). Normalement oui : seule l’exécution PHP est bloquée.
  • Après restauration, vérifiez les caches (page cache, object cache, CDN). Un cache peut “ressusciter” une redirection malveillante côté client.

Contrôles automatiques recommandés

  • WP‑CLI en cron : wp core verify-checksums + alerte si diff.
  • Monitoring fichiers : alerte sur création de .php dans uploads et modification de drop-ins.
  • Journalisation : conservez 30 jours de logs d’accès/erreurs minimum, sinon vous enquêtez à l’aveugle.

Ressources

FAQ

Comment distinguer un faux positif d’une vraie backdoor ?

Un indicateur isolé (ex : base64_decode()) ne suffit pas. Une backdoor combine souvent obfuscation (base64/gzinflate/rot13), exécution (eval/assert), et déclenchement conditionnel (paramètre GET, cookie, user-agent). La corrélation avec les logs (requêtes vers le fichier) est souvent décisive.

Pourquoi interdire PHP dans uploads est si efficace ?

Parce que beaucoup d’attaques finissent par “déposer un fichier”. Si PHP ne s’exécute pas dans uploads, un fichier cache.php devient un simple téléchargement, pas une porte d’entrée.

Est-ce que WordPress 6.9.4 “protège” contre les backdoors ?

Le core est nettement plus robuste qu’il y a quelques années, mais une backdoor passe rarement par une vulnérabilité core moderne. Elle passe par un plugin vulnérable, un compte compromis, ou un serveur mal configuré. D’où l’importance des checksums + durcissement serveur.

WP‑CLI est-il obligatoire ?

Non, mais sans WP‑CLI vous perdez des outils clés (checksums, audit cron, requêtes DB). Sur un site avancé, je le considère comme un prérequis opérationnel.

Que faire si je ne peux pas accéder aux logs (hébergement mutualisé) ?

Demandez l’accès aux logs à l’hébergeur. À défaut, activez une journalisation applicative (plugin de logs) temporairement, mais attention : un site compromis peut falsifier ce qu’il vous montre. Les logs serveur restent la source la plus fiable.

Les plugins de sécurité suffisent-ils ?

Ils aident (WAF, alertes, monitoring), mais ils ne remplacent pas une restauration propre ni le durcissement (uploads, drop-ins, rotation des clés). J’ai déjà vu des backdoors qui désactivaient silencieusement le plugin de sécurité via une option en base.

Dois-je changer les “salts” WordPress après une compromission ?

Oui. Changer les clés de sécurité invalide les cookies et sessions existants. Référez-vous à la doc officielle sur les Security Keys.

Une backdoor peut-elle être uniquement en base de données ?

Oui. Via des options autoloadées, des snippets stockés en DB, ou du contenu injecté dans des widgets/blocs. C’est moins “visible” qu’un fichier, et c’est pour ça que l’audit wp_options est utile.

Quelles zones vérifier en priorité sur un site Elementor/Divi/Avada ?

wp-content/uploads, mu-plugins, drop-ins de cache, et la liste des addons. Les builders génèrent beaucoup de fichiers et de métadonnées, ce qui rend les anomalies plus difficiles à repérer : vous avez besoin d’une baseline (checksums + inventaire).

Pourquoi éviter de “nettoyer” un plugin à la main ?

Parce que vous ne savez pas ce qui a été modifié, et parce que vous risquez de laisser une persistance. Supprimez le dossier du plugin et réinstallez depuis une source sûre. Si c’est un plugin premium, retéléchargez depuis le compte éditeur.

Quel est le meilleur test après nettoyage ?

Un test combiné : checksums core OK, aucun PHP dans uploads, aucun événement cron inconnu, aucun admin suspect, et surveillance des logs 48–72h. Si quelque chose “revient”, la cause (point d’entrée) n’est pas traitée.