Si vous avez déjà vu une alerte “Reflected XSS” dans un rapport WAF ou un pic de requêtes bizarres sur /wp-admin/admin-ajax.php, vous avez probablement frôlé (ou subi) un XSS. Sur WordPress 6.9.4 (avril 2026), le cœur est plutôt solide, mais les thèmes, plugins et snippets “vite faits” restent la cause n°1 que je rencontre en audit.
La menace
Un XSS (Cross-Site Scripting) permet à un attaquant d’exécuter du JavaScript dans le navigateur de vos visiteurs (ou dans celui d’un administrateur). Ce n’est pas “juste une alerte” : c’est un moyen de voler des sessions, détourner des formulaires, injecter des liens SEO, ou déclencher des actions à la place d’un utilisateur connecté.
Concrètement, si un XSS s’exécute dans le contexte d’un administrateur WordPress :
- Vol de cookies / jetons (selon configuration et attributs des cookies), puis usurpation de session.
- Création d’un nouvel admin via des requêtes authentifiées envoyées depuis le navigateur de l’admin (souvent combiné à CSRF si les nonces sont mal gérés).
- Injection persistante dans un post, un widget, un champ ACF, un template builder (Divi/Elementor/Avada), qui touche ensuite tous les visiteurs.
- Redirections et “SEO spam” (liens cachés, pages satellites, scripts de cloaking).
La fréquence réelle dépend du parc plugins. Dans ma pratique, les XSS WordPress “modernes” viennent surtout de :
- sorties HTML non échappées (oubli de
esc_html(),esc_attr(),esc_url()) ; - sanitization trop permissive (ex.
wp_kses_post()là où il faut du texte brut) ; - endpoints AJAX/REST custom qui renvoient des valeurs utilisateur sans contrôle ;
- builders (Divi 5 / Elementor / Avada) : modules/widgets custom qui affichent des options sans échappement, ou autorisent du HTML/JS via des champs “texte”.
Pour situer le cadre : WordPress core publie régulièrement des correctifs de sécurité. Vous pouvez suivre les changements via les annonces officielles et le dépôt GitHub du core (github.com/WordPress/wordpress-develop) ainsi que Trac (core.trac.wordpress.org). Les détails de vulnérabilités sont parfois volontairement limités au moment du patch, ce qui est normal.
Résumé rapide
- Échappez à la sortie : presque tout XSS que je corrige vient d’un
echosansesc_*. - Sanitisez à l’entrée :
sanitize_text_field(),sanitize_email(),sanitize_key(),absint(), etc. - Nonces + capacités sur les endpoints (AJAX/REST/admin-post) : sinon un XSS devient un tremplin.
- Évitez d’imprimer des données dans du JS inline sans
wp_json_encode()/wp_add_inline_script(). - Déployez des en-têtes (CSP au minimum “report-only” au départ) pour réduire l’impact.
- Auditez les builders : modules Divi/Elementor/Avada custom = zone rouge si le dev n’est pas strict sur l’échappement.
Code vulnérable — ce qu’il ne faut PAS faire
Exemple réaliste : un mini-plugin ajoute un shortcode [profile_badge] qui affiche un badge selon un paramètre label venant de l’URL. J’ai vu ce pattern dans des snippets “marketing” collés dans un plugin de snippets.
<?php
/**
* Plugin Name: Profile Badge (vulnérable)
* Description: Exemple pédagogique : ne pas utiliser en production.
*/
add_shortcode('profile_badge', function ($atts) {
// ERREUR : lecture directe de l'URL, sans sanitization
$label = isset($_GET['label']) ? $_GET['label'] : 'Membre';
// ERREUR : sortie directe dans du HTML (XSS réfléchi)
return '<div class="profile-badge">Badge : ' . $label . '</div>';
});
Ce qui se passe en coulisses :
- Le navigateur demande une page contenant le shortcode.
- WordPress exécute le shortcode côté serveur et concatène
$labeldans la réponse HTML. - Si
labelcontient du HTML/JS, il est renvoyé tel quel au navigateur.
Pourquoi c’est exploitable sans “outil” :
- Un attaquant peut pousser un lien piégé (mail, DM, commentaire, QR code) vers une page qui contient le shortcode.
- Si la victime clique, le script s’exécute dans le contexte du domaine.
Variante encore plus fréquente (et plus grave) : XSS persistant via une option enregistrée en base (Customizer, options page, meta utilisateur, etc.). Exemple : une page d’options enregistre un champ “footer_text” et l’affiche ensuite partout.
<?php
// ERREUR : enregistrement d'une option sans sanitization stricte
if (isset($_POST['footer_text'])) {
update_option('my_footer_text', $_POST['footer_text']);
}
// ERREUR : affichage sans échappement
add_action('wp_footer', function () {
echo '<div class="site-footer-text">' . get_option('my_footer_text') . '</div>';
});
Le XSS persistant est celui que je redoute le plus : une fois stocké, il touche tous les visiteurs, et surtout il finit souvent par s’exécuter dans le navigateur d’un admin (qui visite le front), ce qui ouvre la porte à une compromission complète.
Pièges qui rendent ces snippets “accidentellement” pires
- Copier le code au mauvais endroit : collé dans
functions.phpd’un thème parent (perdu à la mise à jour), ou dans un plugin de snippets qui s’exécute aussi dans l’admin. - Tester en production sans sauvegarde : une seule erreur de syntaxe (point-virgule oublié) et c’est écran blanc.
- Utiliser un hook inadapté : afficher dans
admin_headune donnée front non échappée, et vous venez de déplacer le risque dans l’admin. - Conflit cache : vous corrigez le code, mais le cache page/CDN sert encore l’ancienne réponse vulnérable.
Code sécurisé — la bonne implémentation
Objectif : même fonctionnalité (badge personnalisable), mais avec des garanties :
- sanitization à l’entrée (
sanitize_text_field()) ; - échappement à la sortie (
esc_html()) ; - optionnel : liste blanche (allowlist) si la valeur doit être limitée ;
- pas de mélange dangereux HTML/JS ;
- si on stocke : validation stricte + capacités + nonce.
Shortcode sécurisé (XSS réfléchi)
<?php
/**
* Plugin Name: Profile Badge (sécurisé)
* Description: Exemple pédagogique compatible WordPress 6.9.4+ / PHP 8.1+.
*/
add_shortcode('profile_badge', function ($atts) {
// 1) Lire la source (URL) et sanitiser immédiatement
$label_raw = isset($_GET['label']) ? (string) $_GET['label'] : 'Membre';
$label = sanitize_text_field(wp_unslash($label_raw));
// 2) Option : limiter à une allowlist si c'est un "badge" connu
$allowed = array('Membre', 'VIP', 'Auteur', 'Modérateur');
if (!in_array($label, $allowed, true)) {
$label = 'Membre';
}
// 3) Échapper à la sortie selon le contexte (texte HTML)
return '<div class="profile-badge">Badge : ' . esc_html($label) . '</div>';
});
Explication simple :
wp_unslash()gère le slashing automatique des superglobales dans WordPress.sanitize_text_field()retire/normalise des caractères dangereux pour un champ texte.esc_html()empêche le navigateur d’interpréter la valeur comme du HTML.
Explication technique :
- Vous sanitisez pour réduire la surface (données propres, attendues).
- Vous échappez pour garantir que le contexte de rendu (HTML, attribut, URL, JS) reste sûr même si une donnée “sale” passe.
- La allowlist est un cran au-dessus : vous transformez un problème “texte libre” en “valeur contrôlée”.
Références officielles utiles : la doc d’échappement et de sanitization sur Developer Resources (developer.wordpress.org/apis/security/sanitizing/ et developer.wordpress.org/apis/security/escaping/).
Stockage sécurisé (XSS persistant) : options + nonce + capacités
Si vous devez stocker un “footer text”, décidez d’abord : texte brut, ou HTML autorisé ? Le choix change tout. Dans les sites éditoriaux, j’autorise parfois un sous-ensemble HTML (liens, <strong>, <em>), jamais des scripts.
<?php
/**
* Enregistre une option de footer via admin-post.php
* - Capacité : manage_options
* - Nonce : protection CSRF
* - Sanitization : texte ou HTML filtré
*/
add_action('admin_post_my_save_footer', function () {
if (!current_user_can('manage_options')) {
wp_die(__('Accès refusé.', 'my-textdomain'), 403);
}
check_admin_referer('my_save_footer_action', 'my_save_footer_nonce');
$raw = isset($_POST['footer_text']) ? (string) $_POST['footer_text'] : '';
$raw = wp_unslash($raw);
// Choix A : footer en texte brut (le plus sûr)
$footer_text = sanitize_text_field($raw);
// Choix B : autoriser un sous-ensemble HTML (à utiliser seulement si nécessaire)
// $footer_text = wp_kses($raw, array(
// 'a' => array('href' => true, 'title' => true, 'rel' => true, 'target' => true),
// 'strong' => array(),
// 'em' => array(),
// 'br' => array(),
// ));
update_option('my_footer_text', $footer_text, true);
wp_safe_redirect(admin_url('options-general.php?page=my-settings&updated=1'));
exit;
});
add_action('wp_footer', function () {
$footer_text = (string) get_option('my_footer_text', '');
// Si Choix A (texte brut) :
echo '<div class="site-footer-text">' . esc_html($footer_text) . '</div>';
// Si Choix B (HTML filtré) :
// echo '<div class="site-footer-text">' . wp_kses_post($footer_text) . '</div>';
});
Deux détails qui évitent des incidents :
- Ne mélangez pas “HTML filtré” et “texte brut” : si vous stockez du HTML filtré, échappez avec
wp_kses_post()(ou mieux, le mêmewp_kses()avec la même allowlist). Si vous stockez du texte, sortez avecesc_html(). - Ne faites pas confiance à l’admin : beaucoup d’XSS persistants commencent par un rôle “éditeur” trop permissif ou un compte admin compromis.
Cas avancé : données imprimées dans du JavaScript
Le XSS “caché” que je vois souvent : imprimer une valeur utilisateur dans un script inline. Même si vous échappez pour HTML, ce n’est pas le bon contexte. Utilisez wp_json_encode() et wp_add_inline_script().
<?php
add_action('wp_enqueue_scripts', function () {
wp_enqueue_script(
'my-badge',
plugins_url('badge.js', __FILE__),
array(),
'1.0.0',
true
);
$label_raw = isset($_GET['label']) ? (string) $_GET['label'] : 'Membre';
$label = sanitize_text_field(wp_unslash($label_raw));
$data = array(
'label' => $label,
);
// Injection sûre dans JS : JSON encodé, pas de concaténation hasardeuse
$inline = 'window.MyBadgeData = ' . wp_json_encode($data) . ';';
wp_add_inline_script('my-badge', $inline, 'before');
});
Référence : wp_add_inline_script() et bonnes pratiques d’enqueue sur Developer Resources (developer.wordpress.org/reference/functions/wp_add_inline_script/).
Compatibilité Divi 5 / Elementor / Avada : là où ça casse
Sur des sites builder-heavy, le XSS arrive souvent via un module/widget custom :
- Divi 5 : rendu PHP d’un module qui fait
echo $props['title'];sansesc_html(). - Elementor : widget custom qui renvoie
$settings['text']sans échappement dansrender(). - Avada : shortcode Fusion Builder qui imprime des attributs directement dans un
style=""(contexte attribut CSS).
Règle simple : dans un builder, vous n’avez pas moins d’exigences. Vous en avez plus, parce que des utilisateurs non-tech peuvent remplir ces champs.
Configuration serveur
Le serveur ne “corrige” pas un code vulnérable, mais il réduit l’impact. Je recommande d’ajouter des garde-fous progressifs, en testant sur un staging.
.htaccess (Apache) : en-têtes de base
Si vous êtes sur Apache avec mod_headers, vous pouvez poser des en-têtes. Attention : une CSP trop stricte peut casser Divi/Elementor/Avada (scripts inline). Commencez en Report-Only.
# À placer dans le .htaccess (souvent à la racine), en dehors du bloc WordPress si possible
# Testez d'abord sur staging : certains hébergeurs mutualisés limitent mod_headers.
<IfModule mod_headers.c>
# Réduit le risque de MIME sniffing
Header set X-Content-Type-Options "nosniff"
# Empêche l'iframe sur d'autres domaines (anti clickjacking)
Header set X-Frame-Options "SAMEORIGIN"
# Permissions Policy (à ajuster selon besoins)
Header set Permissions-Policy "geolocation=(), microphone=(), camera=()"
# CSP en mode rapport (ne casse pas, mais remonte les violations dans la console)
# Remplacez l'URL par votre endpoint de collecte si vous en avez un.
Header set Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
# Referrer Policy
Header set Referrer-Policy "strict-origin-when-cross-origin"
</IfModule>
Notes terrain :
- Le
'unsafe-inline'en CSP est un compromis. Beaucoup de builders l’exigent encore. L’étape suivante consiste à réduire l’inline via nonces/hashes, mais c’est un chantier (et pas toujours compatible). - Si vous avez un CDN/WAF (Cloudflare, etc.), posez plutôt les headers au niveau CDN pour éviter les divergences.
Nginx : en-têtes équivalents
# Dans le server {} ou location {} du vhost
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
# CSP report-only (à adapter)
add_header Content-Security-Policy-Report-Only "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline' https:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
wp-config.php : durcissement côté WordPress
Ce n’est pas “anti-XSS” directement, mais ça limite les dégâts post-compromission.
<?php
// Désactive l'éditeur de fichiers dans l'admin (réduit l'escalade après intrusion)
define('DISALLOW_FILE_EDIT', true);
// Optionnel : bloque l'installation/mise à jour de plugins/thèmes via l'admin (process plus strict)
define('DISALLOW_FILE_MODS', true);
// Force HTTPS côté admin si votre site est correctement configuré en TLS
define('FORCE_SSL_ADMIN', true);
Référence : constantes de configuration WordPress (developer.wordpress.org/apis/wp-config-php/).
Vérifier si votre site est vulnérable
Vous ne “prouvez” pas l’absence de XSS, mais vous pouvez trouver des signaux forts. Je privilégie trois axes : audit du code (sorties), chasse aux stockages suspects (options/meta), et logs.
WP-CLI : repérer les sorties non échappées (heuristique)
Sur un serveur avec WP-CLI, cherchez les echo $_GET, $_POST, $_REQUEST, et les concaténations HTML.
# Dans wp-content (plugins + thèmes)
cd /path/to/wordpress/wp-content
# Recherches grossières (à affiner) : superglobales imprimées
grep -RIn --exclude-dir=node_modules --exclude-dir=vendor "echo .*\$_GET\|echo .*\$_POST\|echo .*\$_REQUEST" .
# Recherches sur admin-ajax / REST custom
grep -RIn --exclude-dir=node_modules --exclude-dir=vendor "admin-ajax.php\|wp_ajax_\|register_rest_route" .
Ce n’est pas un scanner. C’est une liste de points à relire, en cherchant :
- absence de
sanitize_*à l’entrée ; - absence de
esc_*à la sortie ; - mauvais échappement (ex.
esc_html()dans un attribut, ouesc_attr()dans une URL) ; - valeurs imprimées dans du JS inline sans
wp_json_encode().
SQL : trouver des charges suspectes stockées
Vous cherchez des patterns, pas une certitude. Faites-le sur une copie (ou au minimum en lecture seule). Remplacez wp_ par votre préfixe.
-- Options : recherche de balises script / handlers inline
SELECT option_name, LEFT(option_value, 200) AS preview
FROM wp_options
WHERE option_value LIKE '%<script%'
OR option_value LIKE '%onerror=%'
OR option_value LIKE '%onload=%'
LIMIT 50;
-- Post meta (ACF, builders, etc.)
SELECT meta_key, LEFT(meta_value, 200) AS preview
FROM wp_postmeta
WHERE meta_value LIKE '%<script%'
OR meta_value LIKE '%onerror=%'
OR meta_value LIKE '%onload=%'
LIMIT 50;
Attention : beaucoup de builders stockent du JSON avec du HTML encodé. Un résultat ne veut pas dire “hack”, mais ça mérite vérification.
Logs : ce qui doit vous alerter
- Requêtes répétées avec paramètres qui ressemblent à du HTML/JS (même si encodé URL).
- Pics sur
admin-ajax.php,admin-post.php, endpoints REST custom. - Erreurs 403/406 WAF suivies de 200 sur un autre endpoint (contournements).
- Dans les logs JS (console) : violations CSP si vous avez activé Report-Only (utile pour cartographier l’inline).
Tableau de diagnostic (symptômes terrain)
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| Pop-ups/redirects uniquement pour certains visiteurs | XSS conditionnel (user-agent, referrer) ou injection via cache | Comparer HTML servi (view-source) + désactiver cache/CDN temporairement | Nettoyer la source (option/meta/template) + purger tous les caches |
| Un admin “se déconnecte” ou voit des actions non demandées | XSS exécuté en session admin, ou extension navigateur malveillante | Audit des plugins récents + logs d’actions admin + tester en navigateur vierge | Corriger l’échappement + révoquer sessions + MFA + rotation secrets |
| Liens SEO cachés dans le footer | Option ou widget compromis (XSS persistant ou injection PHP) | Recherche SQL dans wp_options / widgets + diff des fichiers thème |
Nettoyage + durcissement + limiter HTML autorisé |
| Le correctif “ne marche pas” | Cache page/CDN, opcode cache, ou mauvais fichier modifié | Désactiver cache, purger CDN, vérifier le chemin exact du fichier | Purger + déployer correctement + invalider opcode si nécessaire |
Erreurs de sécurité fréquentes
| Erreur | Risque | Solution |
|---|---|---|
Échapper avec la mauvaise fonction (ex. esc_html() dans un href) |
Contournements, HTML cassé, URLs malformées | Utiliser esc_url() pour URLs, esc_attr() pour attributs, esc_html() pour texte |
| Sanitiser à la sortie au lieu de l’entrée | Données dangereuses stockées (XSS persistant) | Sanitiser dès la réception (sanitize_*), échapper au rendu |
Utiliser wp_kses_post() “par défaut” partout |
Autorise plus de HTML que nécessaire | Préférer texte brut ; sinon wp_kses() avec allowlist minimale |
Oublier wp_unslash() sur $_POST/$_GET |
Sanitization incohérente, contournements subtils | Appliquer wp_unslash() avant sanitization |
Endpoints AJAX/REST sans current_user_can() et sans nonce |
Escalade (XSS + actions admin), abus de ressources | Vérifier capacités + check_ajax_referer() / nonces REST |
| Copier un ancien tutoriel (fonctions obsolètes, patterns dangereux) | Régression sécurité sur WordPress 6.9.4 | Recouper avec Developer Resources et le code core actuel |
| Utiliser un hook inadapté / priorité hasardeuse | Échappement contourné par un autre filtre, rendu inattendu | Isoler la responsabilité (filtre vs action) et documenter la priorité |
| Snippet cassé par thème enfant / plugin de snippets | Erreur fatale, ou code exécuté dans l’admin sans contrôle | Déployer en plugin mu-plugin ou plugin standard versionné |
| PHP trop ancien en prod | Correctifs impossibles, libs sécurité manquantes | Monter à PHP 8.1+ (idéalement plus récent si support hébergeur) |
Checklist de durcissement
- Inventaire : listez plugins/thèmes actifs, supprimez ceux inactifs (pas juste “désactiver”).
- Mises à jour : core WordPress 6.9.4 + plugins + thèmes, puis vérifiez le changelog sécurité.
- Échappement : audit ciblé des
echodans vos personnalisations (thème enfant, mu-plugins, snippets). - Sanitization : audit des
update_option(),update_post_meta(),wp_insert_post()avec données utilisateur. - Nonces : toutes les actions sensibles (admin-post, AJAX, formulaires front) doivent vérifier nonce + capacité.
- REST API : vérifiez
permission_callbacksur chaque route custom. - Headers : activez au minimum
nosniff,SAMEORIGIN, et CSP en report-only au départ. - Cookies : forcez HTTPS, vérifiez HSTS si possible (au niveau serveur/CDN).
- Rôles : réduisez les comptes admin, vérifiez les capacités des rôles custom.
- Builders : passez en revue widgets/modules custom (Elementor/Divi/Avada) : chaque champ = sanitization + échappement au bon contexte.
- Cache : après correctif, purgez cache plugin + cache serveur + CDN + navigateur.
- Staging : testez les changements de sécurité (CSP notamment) hors production.
Que faire si le site est déjà compromis ?
Quand un XSS a servi de point d’entrée, il y a souvent autre chose derrière (compte admin ajouté, plugin backdoor, tâches cron). Voici un plan que j’applique en intervention.
- Mettre en maintenance (ou au minimum bloquer l’admin) le temps de l’analyse. Si vous avez un WAF/CDN, limitez
/wp-adminà votre IP temporairement. - Sauvegarder pour preuve : dump base + archive
wp-content+ logs (accès + erreurs). Ne nettoyez pas avant d’avoir une copie, sinon vous perdez la trace. - Révoquer les sessions : changez tous les mots de passe (WP, FTP/SSH, DB), régénérez les clés de sécurité dans
wp-config.php(wordpress.org/documentation/article/editing-wp-config-php/#security-keys). - Vérifier les comptes : listez les utilisateurs admin, cherchez un compte inconnu, vérifiez les emails et dates de création.
- Comparer le core : remplacez WordPress core par une copie propre (même version) sauf
wp-config.phpetwp-content. Idem pour plugins/thèmes : réinstallez depuis sources officielles. - Chasser le XSS persistant : options/widgets, postmeta, templates builder. Faites des recherches SQL (section précédente) et inspectez les champs “rich text”.
- Inspecter
mu-pluginset tâches cron : c’est un endroit classique pour une persistance discrète. - Nettoyer puis patcher : corrigez la vulnérabilité (échappement/sanitization/nonce) avant de rouvrir.
- Purger caches : plugin cache, object cache, CDN, navigateur. J’ai déjà vu un XSS “revenant” uniquement parce que Cloudflare servait une page mise en cache.
- Surveiller : activez logs, ajoutez alerting (WAF, integrity monitoring). Surveillez 7 à 14 jours.
Deux points qui font gagner du temps :
- Si vous utilisez Elementor/Divi/Avada, exportez la page suspecte et cherchez la charge dans le JSON/markup stocké. Ça évite de “cliquer partout”.
- Ne vous contentez pas de supprimer le
<script>visible. Cherchez les event handlers (onload,onerror) et les URLsjavascript:dans les attributs.
Conseils de maintenance et compatibilité
Sur WordPress 6.9.4, vous avez un socle moderne. Le risque vient surtout des personnalisations. Pour éviter les régressions :
Versionnez vos snippets, évitez le “copier-coller” permanent
Quand je vois un site avec 30 snippets dans un plugin “Code Snippets”, je sais que la prochaine mise à jour va casser quelque chose. Préférez :
- un mu-plugin (chargé tôt, stable) pour les fonctions transverses ;
- un plugin custom versionné (Git), avec revue de code ;
- des tests basiques (au moins lint PHP) en CI.
Pattern “service container” léger pour isoler la sécurité
Sur des projets avancés, j’isole la logique de validation/échappement dans des services. WordPress n’impose pas un container, mais vous pouvez en faire un minimaliste pour éviter la duplication et les oublis.
<?php
/**
* Exemple minimal : conteneur simple + service de validation.
* Objectif : centraliser les règles (allowlists, sanitizers).
*/
final class My_Container {
private array $services = array();
public function set(string $id, callable $factory): void {
$this->services[$id] = $factory;
}
public function get(string $id) {
if (!isset($this->services[$id])) {
throw new RuntimeException('Service introuvable : ' . $id);
}
$service = $this->services[$id];
// Cache l'instance après création
if (is_callable($service)) {
$this->services[$id] = $service($this);
}
return $this->services[$id];
}
}
final class My_Label_Sanitizer {
public function sanitize_from_query(string $raw): string {
$label = sanitize_text_field(wp_unslash($raw));
$allowed = array('Membre', 'VIP', 'Auteur', 'Modérateur');
return in_array($label, $allowed, true) ? $label : 'Membre';
}
}
add_action('plugins_loaded', function () {
$container = new My_Container();
$container->set('label_sanitizer', function () {
return new My_Label_Sanitizer();
});
add_shortcode('profile_badge', function () use ($container) {
$raw = isset($_GET['label']) ? (string) $_GET['label'] : 'Membre';
$label = $container->get('label_sanitizer')->sanitize_from_query($raw);
return '<div class="profile-badge">Badge : ' . esc_html($label) . '</div>';
});
});
Ce n’est pas “pour faire joli”. Ça évite surtout qu’un dev ajoute un second affichage du label ailleurs… sans échappement.
Performance/SEO : l’impact des mesures anti-XSS
- CSP : peut révéler énormément d’inline JS/CSS généré par les builders. Traitez ça comme une dette technique. Commencez en report-only, corrigez, puis durcissez.
- Échappement : coût négligeable comparé au rendu builder. Le vrai coût, c’est l’incident.
- Nettoyage DB : après compromission, surveillez les redirections, canonical, et sitemaps. Les injections XSS s’accompagnent parfois de spam SEO.
Ressources
- Sanitizing Data (Developer Resources)
- Escaping Data (Developer Resources)
- check_admin_referer() (référence)
- current_user_can() (référence)
- wp_json_encode() (référence)
- wp_add_inline_script() (référence)
- WordPress Core Trac (suivi des tickets)
- Dépôt GitHub wordpress-develop
- Hardening WordPress (documentation)
- PHP: filtres de sanitization (référence)
FAQ
Quelle est la différence pratique entre XSS réfléchi et persistant ?
Le réfléchi vient d’une requête (URL/formulaire) et n’existe pas en base. Le persistant est stocké (options, postmeta, contenu) et s’exécute à chaque affichage. En incident réel, le persistant est plus destructeur et plus long à éradiquer.
“J’ai échappé avec esc_html() partout”, c’est suffisant ?
Non, parce que le contexte compte. Dans un href, utilisez esc_url(). Dans un attribut, esc_attr(). Dans du JS, encodez en JSON via wp_json_encode(). Et il faut aussi sanitiser à l’entrée, sinon vous stockez des charges qui ressortiront ailleurs.
Est-ce que wp_kses_post() protège contre tous les XSS ?
Ça filtre un sous-ensemble de HTML, mais ce n’est pas une baguette magique. D’abord, vous ne devriez pas autoriser du HTML si vous n’en avez pas besoin. Ensuite, certaines erreurs viennent du contexte (attributs, JS inline) où wp_kses_post() n’est pas le bon outil.
Pourquoi recommander nonces + capacités si on parle de XSS ?
Parce qu’un XSS dans le navigateur d’un utilisateur connecté peut déclencher des requêtes “légitimes”. Si vos endpoints n’ont pas de nonce/capacité, l’attaquant transforme un XSS en exécution d’actions privilégiées. C’est un multiplicateur de dégâts.
Une CSP suffit-elle à “bloquer” les XSS ?
Une CSP bien configurée réduit fortement l’impact, mais elle ne remplace pas l’échappement/sanitization. Et sur des sites avec builders, une CSP stricte est souvent un projet progressif. Commencez par Report-Only, corrigez, puis durcissez.
Quels sont les endroits WordPress où un XSS persistant se cache le plus souvent ?
wp_options (widgets, options de thème), wp_postmeta (ACF, builders), templates de thème enfant, et modules/widgets custom. J’ajoute aussi : scripts injectés via “header/footer scripts” d’un plugin.
Comment éviter les régressions quand plusieurs développeurs touchent au thème enfant ?
Centralisez les helpers (sanitizers/escapers), imposez une checklist de revue (contexte d’échappement), et versionnez. Sur des équipes, je préfère un plugin custom à un thème enfant “fourre-tout”.
Que faire si mon correctif ne semble pas pris en compte ?
Vérifiez d’abord le cache (plugin, serveur, CDN), puis assurez-vous d’avoir modifié le bon fichier (thème enfant vs parent). J’ai vu des corrections appliquées au parent, puis écrasées à la mise à jour. Purgez aussi l’opcache si vous contrôlez le serveur.
Est-ce dangereux d’autoriser du HTML dans un champ “texte” d’un builder ?
Oui, surtout si ce champ est modifiable par des rôles non-admin. Si vous devez autoriser du HTML, utilisez une allowlist stricte via wp_kses(), et sortez avec la même logique. N’autorisez jamais des scripts.
Comment auditer rapidement un plugin suspect ?
Cherchez les superglobales ($_GET/$_POST), les sorties (echo), et les endpoints (wp_ajax_*, register_rest_route). Puis vérifiez : sanitization à l’entrée, échappement au bon contexte, nonce/capacités sur les actions sensibles.