Si vous avez déjà vu un pic de requêtes /wp-admin/admin-ajax.php ou des tentatives de connexion sur xmlrpc.php dans vos logs, vous avez déjà observé WordPress “sous attaque” — même sur un petit blog.

Ce guide vise WordPress 6.9.4 (avril 2026) et PHP 8.1+. Je me concentre sur le durcissement “dans WordPress” (rôles, permissions, code, plugins, REST, admin-ajax, uploads), puis sur la couche serveur qui évite que la moindre erreur applicative devienne une compromission.

La menace

Un attaquant n’a pas besoin de “hacker WordPress” au sens hollywoodien. Dans la pratique, il exploite un point faible banal : un plugin vulnérable, un compte admin réutilisant un mot de passe compromis, une API interne exposée, ou un snippet copié-collé sans nonce.

Ce qu’il peut faire si votre site est vulnérable :

  • Prendre le contrôle d’un compte (admin ou éditeur) et publier du contenu spam, injecter des liens, modifier des redirections.
  • Exécuter du code via une faille d’upload, une désérialisation, ou une inclusion de fichier. C’est le scénario “pire cas”.
  • Voler des données (emails, IP, formulaires) ou siphonner des tokens (cookies, nonces si mal gérés).
  • Installer une porte dérobée (webshell, mu-plugin caché, tâche cron malveillante) et revenir après “nettoyage”.
  • Monétiser votre trafic : SEO spam, redirections conditionnelles, injection de scripts, popups frauduleux.

La fréquence réelle : les tentatives automatisées sont quotidiennes. Même un site sans trafic reçoit des scans. Les vecteurs les plus courants restent (1) identifiants compromis, (2) plugins/thèmes obsolètes, (3) mauvaises pratiques de code (AJAX/REST sans permissions), (4) serveur mal configuré (écriture partout, headers manquants).

Le risque en langage simple : WordPress est une application web très extensible. Chaque extension ajoute des routes, des formulaires, des endpoints, des capacités. Si une seule brique oublie un contrôle d’accès ou une validation, votre site devient “le maillon faible” du botnet qui scanne Internet.

Résumé rapide

  • Moins de surface d’attaque : désinstallez ce qui ne sert pas, limitez les rôles, coupez XML-RPC si inutile.
  • Chaque action sensible = nonce + capability (admin, AJAX, REST). Pas l’un sans l’autre.
  • Échappez en sortie, validez en entrée : sanitize_* à la réception, esc_* à l’affichage.
  • Uploads et fichiers : types MIME stricts, pas d’exécution PHP dans wp-content/uploads.
  • Durcissez le serveur : headers, permissions, désactivation de l’édition de fichiers, restrictions d’accès.
  • Surveillez : WP-CLI, logs, alertes, et un plan de restauration testé.

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

Voici un pattern que je vois encore sur des sites “intermédiaires” : un endpoint AJAX qui met à jour une option, sans capability check, avec un nonce absent ou mal vérifié, et des données non validées. Ça finit souvent en défacement ou en redirection SEO spam.

<?php
/**
 * Plugin: Exemple vulnérable (NE PAS UTILISER)
 * Problème : absence de contrôle de permission, nonce incorrect, validation faible.
 */

add_action('wp_ajax_nopriv_bpcab_save_settings', 'bpcab_save_settings_vulnerable');
add_action('wp_ajax_bpcab_save_settings', 'bpcab_save_settings_vulnerable');

function bpcab_save_settings_vulnerable() {
	// Erreur fréquente : "je mets un nonce plus tard" → oublié en production
	// Erreur : check_ajax_referer() absent ou mauvais paramètre

	// Données non validées
	$redirect_url = $_POST['redirect_url'] ?? '';

	// Erreur critique : met à jour une option globale accessible à tous
	update_option('bpcab_redirect_url', $redirect_url);

	wp_send_json_success(array(
		'message' => 'OK',
	));
}

Comment l’attaque fonctionne (sans outillage) :

  • Le endpoint est accessible aux visiteurs (wp_ajax_nopriv_*).
  • Il n’y a aucun contrôle d’accès : n’importe qui peut appeler l’action AJAX.
  • La valeur est stockée telle quelle. Si votre thème ou un plugin réutilise ensuite cette option dans un wp_redirect() ou un lien, l’attaquant peut injecter une redirection vers un site malveillant.

J’ai souvent vu ce problème sur des sites avec Elementor/Divi : un snippet “réglages du thème” est ajouté via un plugin de snippets, puis un module affiche un bouton “Lire plus” qui utilise l’option. Une seule option empoisonnée, et vous avez une redirection conditionnelle sur tout le site.

Code sécurisé — la bonne implémentation

On reprend la même fonctionnalité (enregistrer une URL de redirection), mais avec les protections attendues sur WordPress 6.9.4 :

  • Nonce vérifié côté serveur.
  • Capability check : seuls les admins (ou un rôle précis) peuvent modifier l’option.
  • Validation stricte : on accepte uniquement une URL interne (ou une liste blanche).
  • Pas d’accès public si ce n’est pas nécessaire.

1) Enqueue du script + nonce côté admin

Piège réaliste : beaucoup de gens localisent le nonce sur le mauvais hook (ex: wp_enqueue_scripts au lieu de admin_enqueue_scripts), ou oublient de vider le cache (plugin de cache + cache navigateur) et pensent que “le nonce ne marche pas”.

<?php
/**
 * Plugin: Exemple sécurisé (WP 6.9.4+, PHP 8.1+)
 */

add_action('admin_enqueue_scripts', function($hook) {
	// Chargez uniquement sur votre page d’options pour limiter l’exposition
	if ($hook !== 'settings_page_bpcab-security') {
		return;
	}

	wp_enqueue_script(
		'bpcab-security-admin',
		plugins_url('assets/admin.js', __FILE__),
		array(),
		'1.0.0',
		true
	);

	wp_localize_script('bpcab-security-admin', 'BPCAB_SECURITY', array(
		'ajaxUrl' => admin_url('admin-ajax.php'),
		'nonce'   => wp_create_nonce('bpcab_save_settings'),
	));
});

2) Endpoint AJAX sécurisé (admin uniquement)

Ici, on n’enregistre pas l’action nopriv. Si vous avez un besoin réel côté front, vous devez compenser par un modèle d’autorisations différent (ex: utilisateur connecté + capability spécifique, ou token applicatif). Par défaut, évitez.

<?php
add_action('wp_ajax_bpcab_save_settings', 'bpcab_save_settings_secure');

function bpcab_save_settings_secure() {
	// 1) Nonce : protège contre les requêtes CSRF depuis un navigateur connecté
	check_ajax_referer('bpcab_save_settings', 'nonce');

	// 2) Permissions : protège contre les comptes connectés non autorisés
	if (!current_user_can('manage_options')) {
		wp_send_json_error(array(
			'message' => 'Permission refusée.',
		), 403);
	}

	// 3) Validation : on accepte uniquement une URL interne (même domaine)
	$redirect_url_raw = isset($_POST['redirect_url']) ? wp_unslash($_POST['redirect_url']) : '';
	$redirect_url_raw = trim($redirect_url_raw);

	if ($redirect_url_raw === '') {
		update_option('bpcab_redirect_url', '');
		wp_send_json_success(array('message' => 'Réglage effacé.'));
	}

	// Autorise uniquement les URLs "locales" : /chemin ou URL du site
	$redirect_url = bpcab_validate_internal_url($redirect_url_raw);

	if ($redirect_url === null) {
		wp_send_json_error(array(
			'message' => 'URL invalide. Utilisez une URL interne (même domaine).',
		), 400);
	}

	update_option('bpcab_redirect_url', $redirect_url);

	wp_send_json_success(array(
		'message' => 'Réglage enregistré.',
	));
}

/**
 * Valide une URL interne.
 * Retourne l’URL normalisée, ou null si refusée.
 */
function bpcab_validate_internal_url(string $value): ?string {
	// Autoriser les chemins relatifs
	if (str_starts_with($value, '/')) {
		// Nettoyage basique : supprime les doubles espaces, etc.
		$value = preg_replace('~s+~', '', $value);
		return esc_url_raw($value);
	}

	// Autoriser les URLs absolues du même host
	$value = esc_url_raw($value);
	if ($value === '') {
		return null;
	}

	$site_host = wp_parse_url(home_url(), PHP_URL_HOST);
	$val_host  = wp_parse_url($value, PHP_URL_HOST);

	if (!$site_host || !$val_host) {
		return null;
	}

	// Comparaison stricte du host (attention aux sous-domaines)
	if (strcasecmp($site_host, $val_host) !== 0) {
		return null;
	}

	return $value;
}

Pourquoi ça marche (simple puis technique)

Simple : le nonce empêche un site tiers de déclencher l’action à votre place pendant que vous êtes connecté. Le current_user_can() empêche un compte “abonné” ou “auteur” de modifier une option globale. La validation empêche de transformer votre site en plateforme de redirection.

Technique : check_ajax_referer() valide un nonce lié à une action. Le nonce n’est pas un secret “cryptographique” absolu, mais une protection CSRF standard dans WordPress. Le capability check s’appuie sur le système de rôles/capacités. Enfin, esc_url_raw() + wp_parse_url() réduisent les risques d’injection d’URL et d’open redirect.

Cas page builders (Divi 5, Elementor, Avada)

Les page builders n’augmentent pas le risque “par magie”, mais ils poussent souvent à :

  • ajouter des snippets dans un plugin de code, parfois chargé partout ;
  • créer des formulaires (Elementor Forms, Divi Contact Form) qui appellent des webhooks ;
  • afficher des données options/meta dans des widgets dynamiques.

Conseil concret : si vous exposez une option “URL” qui sera utilisée par un bouton Elementor/Divi, validez-la comme ci-dessus (interne uniquement) ou imposez une liste blanche de domaines. Les attaques de type SEO spam adorent les champs “lien” non contrôlés.


Configuration serveur

Le durcissement serveur sert à limiter l’impact quand un plugin a un bug. Ça ne remplace pas les correctifs, mais ça évite le scénario “upload PHP → exécution”.

.htaccess (Apache) : bloquer l’exécution PHP dans uploads

Sur Apache avec AllowOverride, placez un .htaccess dans wp-content/uploads/. Si vous êtes sur Nginx, voyez plus bas.

# À déposer dans wp-content/uploads/.htaccess
# Objectif : empêcher l'exécution de PHP dans les fichiers uploadés
<FilesMatch ".(php|phtml|phar)$">
	Require all denied
</FilesMatch>

# Réduit les fuites d'infos
Options -Indexes

Piège réaliste : certains hébergeurs désactivent AllowOverride. Dans ce cas, votre .htaccess est ignoré. Vérifiez dans les headers/erreurs Apache, ou demandez au support.

Nginx : équivalent pour uploads

À adapter dans le bloc server de votre vhost. Testez la conf avant reload.

# Empêche l'exécution de scripts dans uploads
location ~* ^/wp-content/uploads/.*.(php|phtml|phar)$ {
	deny all;
	return 403;
}

# Optionnel : pas de listing de répertoires
location ^~ /wp-content/uploads/ {
	autoindex off;
}

wp-config.php : durcissement “dans WordPress”

Ces constantes sont toujours pertinentes en 6.9.4. Je les active quasi systématiquement sur les sites clients.

<?php
// Désactive l’éditeur de fichiers dans l’admin (évite l’escalade si un admin est compromis)
define('DISALLOW_FILE_EDIT', true);

// Optionnel : interdit l’installation/mise à jour de plugins/thèmes depuis l’admin
// À activer si vous déployez via Git/CI et que vous voulez verrouiller l’admin.
// define('DISALLOW_FILE_MODS', true);

// Force SSL sur l’admin si votre site est en HTTPS (devrait l’être)
// define('FORCE_SSL_ADMIN', true);

// Limite les révisions (réduit la surface en cas d’injection dans le contenu + performance DB)
define('WP_POST_REVISIONS', 20);

// Désactive l’écriture de debug log en production (évite fuite de chemins/tokens)
define('WP_DEBUG', false);
define('WP_DEBUG_LOG', false);

Headers HTTP (sécurité navigateur)

Ces headers réduisent l’impact d’une XSS et évitent certaines fuites. WordPress ne les impose pas tous par défaut, car ils dépendent du contexte (CDN, iframes, etc.).

# Apache (.htaccess à la racine)
<IfModule mod_headers.c>
	Header always set X-Content-Type-Options "nosniff"
	Header always set Referrer-Policy "strict-origin-when-cross-origin"
	Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
	Header always set X-Frame-Options "SAMEORIGIN"
	# CSP : à tester, peut casser des builders (Elementor/Divi) si trop stricte
	# Header always set Content-Security-Policy "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:;"
</IfModule>

Note terrain : une CSP “propre” casse souvent des modules de page builder (scripts inline, domains tiers). Si vous la mettez, faites-le progressivement (mode Content-Security-Policy-Report-Only d’abord) et surveillez.


Vérifier si votre site est vulnérable

Je préfère un diagnostic “bas niveau” (WP-CLI + logs) plutôt qu’un plugin de scan qui ajoute lui-même une surface d’attaque. Un scanner sérieux a sa place, mais commencez simple.

WP-CLI : vérifier versions, utilisateurs, plugins suspects

Exécutez sur le serveur (SSH). Sauvegardez avant toute action.

# Version WordPress
wp core version

# Plugins et mises à jour disponibles
wp plugin list --format=table
wp plugin list --update=available --format=table

# Thèmes
wp theme list --format=table
wp theme list --update=available --format=table

# Comptes admin (attention : ne publiez pas cette sortie)
wp user list --role=administrator --fields=ID,user_login,user_email,display_name,registered

Ce que vous cherchez :

  • un plugin “abandonné” (pas mis à jour depuis longtemps) ou inconnu ;
  • un thème inactif mais présent (surface d’attaque inutile) ;
  • un admin que personne ne reconnaît, ou un email externe suspect.

WP-CLI : repérer des mu-plugins et tâches cron

Les backdoors aiment les endroits “discrets” : wp-content/mu-plugins et des crons qui réinjectent du code.

# MU-plugins
wp plugin list --mu --format=table

# Cron (liste complète)
wp cron event list --format=table

SQL : repérer options anormales (injections, autoload)

Si vous avez accès SQL (ou via wp db query), cherchez :

  • des options siteurl/home modifiées ;
  • des options autoload énormes (souvent signe de spam/injection, et tue la perf).
# Attention : adaptez le préfixe si besoin
wp db query "SELECT option_name, LENGTH(option_value) AS size, autoload
FROM wp_options
ORDER BY size DESC
LIMIT 20;"

Logs : signaux concrets

  • Access logs : rafales sur /wp-login.php, /xmlrpc.php, /wp-admin/admin-ajax.php, endpoints REST inconnus.
  • Error logs : erreurs PHP répétées dans un plugin précis (souvent corrélé à une faille), warnings de fichiers créés dans uploads.
  • Mail logs : envoi massif (compte compromis, formulaire détourné).

Tableau de diagnostic

Symptôme Cause probable Vérification Solution
Redirections aléatoires vers un site tiers Option empoisonnée, injection JS, plugin nulled Inspecter wp_options (siteurl/home), chercher scripts dans header/footer, comparer fichiers du thème Remplacer par sources propres, supprimer plugin nulled, verrouiller options + validation URL
Pages inconnues “casino / pharma” indexées Création de posts via compte compromis ou endpoint REST/AJAX Logs d’édition, liste des auteurs, endpoints, historique des publications Révoquer comptes/tokens, corriger permissions, nettoyer contenu, demander désindexation
Pic CPU sur admin-ajax.php Action AJAX publique, absence de throttling/caching Access logs sur admin-ajax, profiler requêtes, identifier action Supprimer nopriv inutile, vérifier nonce/capabilities, rate limit au niveau serveur/WAF
Fichiers .php dans uploads Faille d’upload ou écriture arbitraire Recherche find wp-content/uploads -name "*.php" Supprimer, bloquer exécution PHP, corriger plugin, scanner intégrité

Erreurs de sécurité fréquentes

Erreur Risque Solution
Copier du code “AJAX” trouvé en ligne sans nonce CSRF + modifications non autorisées check_ajax_referer() + current_user_can() + validation stricte
Enregistrer wp_ajax_nopriv_* “par facilité” Endpoint public exploitable par bots Supprimer nopriv sauf cas justifié, sinon authentification + limites
Utiliser sanitize_text_field() pour une URL Validation inadaptée, open redirect possible esc_url_raw() + contrôle du host (liste blanche)
Oublier wp_unslash() sur $_POST Données mal interprétées, validations contournées Appliquer wp_unslash() avant sanitize/validate
Tester sur production sans sauvegarde Indisponibilité, perte de données Staging + backup + plan de rollback
Snippet ajouté au mauvais endroit (thème parent, mauvais hook) Perte à la mise à jour, exécution trop tôt/trop tard Plugin dédié ou MU-plugin, hooks adaptés, priorité maîtrisée
Conflit avec cache (page cache / object cache) Nonce “expiré”, comportements incohérents Exclure pages d’admin, purger caches, éviter de cacher des pages avec nonces
Utiliser un tutoriel ancien (pré-PHP 8.1 / WP ancien) Fonctions obsolètes, failles réintroduites Vérifier la doc officielle WP 6.9+, tester sur PHP 8.1+
Permissions fichiers trop ouvertes (777) Écriture de backdoors, modifications invisibles 644/755, propriétaire correct, verrouillage sur prod

Checklist de durcissement

  • Mises à jour : WordPress 6.9.4+, plugins/thèmes à jour, supprimer (pas seulement désactiver) ce qui ne sert pas.
  • Comptes : 2FA si possible, mots de passe uniques, supprimer les admins inutiles, limiter les éditeurs.
  • Rôles : ne donnez pas “admin” pour publier un article. Créez un rôle propre si nécessaire.
  • XML-RPC : désactivez-le si vous n’en avez pas besoin (Jetpack/clients mobiles anciens peuvent l’utiliser).
  • REST API : ne bloquez pas “en global” à l’aveugle (ça casse Gutenberg et des builders), mais protégez vos endpoints custom (permissions + validation).
  • AJAX : pas de nopriv inutile, nonce + capability, rate limiting côté serveur si endpoint public.
  • Uploads : blocage exécution PHP dans uploads, types MIME stricts, pas d’éditeur de fichiers.
  • wp-config.php : DISALLOW_FILE_EDIT, clés uniques à jour, debug off en prod.
  • Headers : nosniff, SAMEORIGIN, Referrer-Policy, CSP progressive si possible.
  • Sauvegardes : quotidiennes minimum, externalisées, test de restauration mensuel.
  • Monitoring : alertes de login, changements de fichiers, et vérification régulière des crons/mu-plugins.

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

Si vous suspectez une compromission, évitez le réflexe “je supprime deux fichiers et c’est bon”. Les attaquants laissent souvent une persistance (cron, mu-plugin, utilisateur admin, clé API).

  1. Isoler : mettez le site en maintenance (ou au minimum protégez l’admin par IP/VPN) et stoppez l’hémorragie (désactivez temporairement la création de comptes, coupez les formulaires si nécessaire).
  2. Sauvegarder l’état actuel : faites une copie des fichiers + dump DB avant nettoyage. Ça sert pour l’analyse et, parfois, pour prouver l’incident.
  3. Changer les secrets :
    • mots de passe WordPress (tous les admins),
    • mots de passe FTP/SSH/hosting,
    • mots de passe DB,
    • clés AUTH_KEY/SECURE_AUTH_KEY… dans wp-config.php (force la déconnexion des sessions).
  4. Identifier le point d’entrée :
    • plugin/thème nulled ou obsolète,
    • compte admin inconnu,
    • faille d’upload,
    • endpoint AJAX/REST custom sans permission.
  5. Remplacer par des sources propres :
    • réinstallez WordPress core depuis une source officielle (ne “nettoyez” pas à la main),
    • réinstallez plugins/thèmes depuis wordpress.org ou l’éditeur,
    • supprimez tout fichier inconnu, surtout dans wp-content/uploads, mu-plugins, wp-includes.
  6. Nettoyer la base :
    • supprimez utilisateurs suspects,
    • inspectez wp_options (siteurl/home, options de cache, injections),
    • recherchez du contenu injecté (scripts, iframes) dans posts/widgets.
  7. Vérifier la persistance :
    • wp cron event list (tâches étranges),
    • MU-plugins,
    • tâches système (crontab serveur) si vous avez accès.
  8. Mettre à jour et durcir : appliquez la checklist, ajoutez les règles uploads/headers, retirez les endpoints publics.
  9. Contrôler SEO : vérifiez Google Search Console (pages indexées, actions manuelles), nettoyez les sitemaps, corrigez les redirections.
  10. Documenter : date, vecteur, correctifs. Cette étape évite de répéter l’incident 3 mois plus tard.

Note terrain : si vous êtes sur un hébergement mutualisé et que des sites voisins sont compromis, vous pouvez nettoyer indéfiniment. Dans ce cas, migrez vers un environnement isolé (container, VM, hébergement managé) avant de réouvrir.


Conseils de maintenance et compatibilité

Déployez comme un développeur, pas comme un magicien. Sur les sites intermédiaires, la faille vient souvent d’un “petit snippet” ajouté en urgence, puis oublié. Mettez vos personnalisations dans un plugin dédié (ou MU-plugin) versionné, plutôt que dans le thème parent ou dans 12 snippets dispersés.

Compatibilité Divi 5 / Elementor / Avada : évitez de “bloquer la REST API” globalement. Gutenberg et de nombreux builders s’appuient dessus. Préférez :

  • protéger vos endpoints custom (permissions callback strict),
  • désactiver uniquement ce qui est inutile (XML-RPC, actions AJAX publiques),
  • tester vos headers (CSP) sur un staging avec vos templates.

Performance et sécurité se rejoignent : un site lent est plus difficile à surveiller (logs énormes, timeouts), et un endpoint public non limité peut devenir un DoS applicatif. Si vous avez un endpoint public, mettez un cache, un rate limit (WAF/CDN), et une logique côté serveur qui refuse vite.

Erreurs réalistes à éviter lors des changements :

  • copier le code dans functions.php du thème parent (perdu à la mise à jour) ;
  • oublier un point-virgule et casser tout le site (faites vos ajouts sur staging) ;
  • utiliser un hook trop tôt (ex: appeler current_user_can() avant que l’utilisateur soit chargé) ;
  • ne pas régénérer les permaliens après changement de routes ;
  • ne pas purger le cache après modification des nonces/scripts.

Ressources

FAQ

Faut-il désactiver XML-RPC en 2026 ?

Si vous n’utilisez pas Jetpack, une appli mobile ancienne, ou un service qui s’appuie dessus, oui. XML-RPC est une cible historique de brute force et d’abus. Si vous en avez besoin, protégez-le (WAF/rate limit) et surveillez les logs.

Est-ce que “cacher wp-login.php” suffit ?

Non. Ça réduit un peu le bruit, mais ne corrige pas les failles applicatives, les plugins vulnérables, ou les identifiants compromis. Traitez ça comme une mesure de confort, pas une stratégie.

Nonce seul = sécurisé ?

Non. Le nonce protège surtout contre le CSRF. Il ne remplace pas un contrôle de permission. La paire attendue est : nonce + capability.

Pourquoi éviter wp_ajax_nopriv ?

Parce que vous créez un endpoint public. S’il fait une opération coûteuse (requêtes, génération PDF, envoi mail) ou sensible (écriture en DB), il sera scanné et abusé. Si vous devez le faire, ajoutez validation stricte, limitation, et idéalement un mécanisme d’authentification.

Je veux bloquer la REST API pour “sécuriser”. Bonne idée ?

Rarement. Vous casserez Gutenberg et parfois Divi/Elementor/Avada selon les modules. Protégez plutôt vos endpoints custom et limitez l’exposition des données (ne rendez pas publiques des infos inutiles).

Quel est le meilleur endroit pour mettre du code de durcissement ?

Un plugin dédié (ou MU-plugin) versionné. Évitez functions.php du thème parent. Sur des sites avec builders, c’est un classique : le thème change, et le durcissement disparaît.

Quels fichiers doivent être non-inscriptibles en production ?

Idéalement, tout sauf wp-content/uploads (et parfois wp-content/cache selon votre stack). Plus vous réduisez l’écriture, plus vous bloquez les backdoors.

Comment savoir si un plugin est “nulled” ou suspect ?

Signaux : provenance hors marketplace officielle, mises à jour impossibles, fichiers obfusqués, appels externes étranges, présence de code encodé. En cas de doute, remplacez par une source officielle.

Une CSP vaut le coup avec Elementor/Divi ?

Oui, mais progressivement. Commencez en Report-Only, observez ce qui casse, puis resserrez. Une CSP trop stricte peut casser des scripts inline que certains modules utilisent.

Que faire si mon cache casse les nonces (erreurs “Are you sure you want to do this?”) ?

Excluez les pages d’admin et toute page front contenant des formulaires avec nonce du cache page. Purgez le cache après déploiement. Sur certains stacks, l’object cache peut aussi conserver des fragments trop longtemps : testez en désactivant temporairement pour isoler.

À quelle fréquence dois-je tester une restauration ?

Au moins une fois par mois, et après tout changement majeur (migration, nouveau builder, refonte). Une sauvegarde non testée est une hypothèse, pas un filet de sécurité.