Quand on parle d’attaque en lien avec WordPress, on pense souvent à des mots de passe faibles, à des sites abandonnés, ou à des plugins orphelins. Pourtant, les XSS (Cross-Site Scripting) restent une des familles les plus tenaces, surtout parce qu’elles se cachent dans les détails du rendu côté navigateur. Une petite omission dans un thème, un champ de plugin affiché trop vite, et votre site peut devenir un relais pour exécuter du JavaScript au sein du contexte du visiteur.
Ce qui rend les XSS particulières, c’est leur côté “indirect”. Le développeur écrit un bout de PHP ou une fonction WordPress, il croit maîtriser le flux de données, puis l’affichage part en HTML avec une échappement insuffisant. L’attaquant, lui, n’a pas besoin de casser l’architecture, il lui suffit de faire passer une charge utile dans un endroit mal encadré: un attribut, une balise script, une URL, un commentaire, un paramètre de requête.
L’objectif de cet article est de vous aider à construire un audit et sécurisation WordPress orienté XSS, concentré sur les thèmes et les plugins. Pas une théorie générale, mais une méthode pragmatique, avec des pièges réalistes, des exemples et des points de vigilance côté WordPress et côté navigateur.
Le modèle mental utile: où naissent les XSS
Une XSS survient quand un contenu contrôlé par un attaquant se retrouve interprété par le navigateur comme du code, au lieu d’être traité comme une simple donnée. Sur WordPress, les sources typiques sont:
- des champs d’options (settings d’un plugin, variables stockées en base), des champs de formulaires (commentaires, profils, métadonnées), des paramètres GET/POST (varia, filtres, recherche), des contenus importés (ex: endpoints d’API, sync, webhooks), des traductions et contenus dynamiques (moins évident, mais réel dans certains thèmes).
La partie qui “fait basculer” vers l’exécution se produit dans le rendu: concaténation dans du HTML, insertion dans des attributs, injection dans du JavaScript inline, construction d’URL, utilisation de filtres et shortcodes qui renvoient du HTML.
Le point clé pour un audit est de repérer les endroits où du texte non échappé traverse des frontières. En pratique, pensez à chaque affichage comme à une frontière de sécurité: vous avez une entrée, puis vous générez une sortie. L’audit doit suivre le trajet jusqu’à la sortie.
Pourquoi les thèmes et plugins sont un terrain fertile
WordPress encourage la modularité. Thèmes et plugins manipulent souvent:
- des shortcodes, des templates avec des variables, des formulaires de configuration dans l’admin, des libellés, traductions, messages d’erreur, des composants qui mélangent HTML et données.
Dans ce contexte, les développeurs finissent parfois par “faire vite”. Une variable insérée dans un attribut, “ça passe chez moi”. Un escape oublié dans un rendu conditionnel. Une concaténation d’URL basée sur $_GET. Rien d’extraordinaire en apparence, et pourtant ce sont ces détails qui déclenchent des XSS.
Un autre facteur est l’héritage. Beaucoup de bases WordPress ont des plugins anciens, des surcouches, des thèmes hérités. On peut avoir un bon plugin “métier”, mais une partie d’affichage qui n’a pas été revue depuis longtemps.
Ce qu’un audit doit viser concrètement
Un audit XSS ne se limite pas à chercher des chaînes du type echo $_POST. Les XSS arrivent aussi via des transformateurs “inoffensifs en apparence”: wp_kses_post, wp_kses, sanitize_text_field, esc_html utilisés au mauvais endroit, ou pas du tout.
Un audit sérieux vise trois choses:
Cartographier les chemins d’entrée vers les sorties: quelles données entrent, puis où elles sont affichées. Valider l’échappement et la sérialisation selon le contexte de sortie: HTML, attribut, URL, script, CSS. Vérifier les “cas de contournement”: balises, caractères spéciaux, encodages, fonctions qui changent le type (texte, HTML, objet, tableau).Vous obtenez alors une liste d’emplacements à corriger, avec une justification. Ce dernier point est important, car un correctif XSS mal ciblé peut casser l’affichage légitime ou ouvrir un autre angle.
Comprendre le contexte d’affichage: la règle la plus souvent mal appliquée
Il n’existe pas une seule fonction d’échappement universelle. Le contexte compte.
- Sortie dans le contenu HTML: l’échappement n’est pas le même que dans un attribut. Sortie dans une chaîne JavaScript inline: la logique change complètement, car vous devez éviter de fermer des guillemets ou d’introduire des caractères interprétés. Sortie dans une URL: vous devez vous protéger à la fois contre les injections et contre des schémas d’URL dangereux. Sortie dans un attribut href, src, ou style: même si vous “échappez”, l’HTML restant interprétable, il faut une validation adaptée.
Sur WordPress, cela se traduit par une approche “choisir l’escape en fonction du contexte”, puis vérifier que le code respecte cette discipline partout.
Signaux d’alerte fréquents dans le code
Sans tomber dans une chasse aux sorcières, certains patterns reviennent souvent dans les audits:
Si vous voyez des variables concaténées dans des templates, sans échappement clair, c’est un drapeau. Si vous voyez des fonctions qui “nettoient” une entrée mais qui sont suivies d’un affichage sans échappement, cela ressemble plus à de la confiance excessive qu’à une protection. Et si vous voyez du JavaScript inline construit à partir de données dynamiques, c’est presque toujours un point de fragilité.
Voici les premiers indicateurs que je traite en priorité lors d’un audit:
- affichage direct avec echo ou print de variables issues de requêtes, de métadonnées, ou d’options sans fonction d’échappement liée au contexte construction de chaînes HTML par concaténation (ex: "".$label."") plutôt que via un rendu contrôlé insertion dans des attributs sensibles comme href, src, title, data-* sans échappement approprié génération de script inline ou de blocs JS qui incluent des données non sérialisées correctement usage de wp_kses sans comprendre exactement quel sous-ensemble est autorisé, puis réutilisation de ce contenu ailleurs sans re-vérifier le contexte
Ces signaux n’accusent pas automatiquement un XSS, mais ils donnent un ordre de travail. En audit, gagner du temps signifie commencer par les surfaces où l’erreur est la plus probable.
Un angle pratique: reproduire sans “sur-inventer”
Avant de corriger, il faut savoir ce qui est réellement exploitable. L’erreur classique, c’est de “patcher” des endroits qui ne sont pas réellement atteignables, ou qui ne souffrent que d’un échappement manquant mais pas d’un scénario d’exécution. C’est là que les tests manuels et l’observation comptent.
En général, je procède comme suit dans un projet existant:
- Identifier où les données arrivent: page publique, page admin, endpoint AJAX, shortcode, bloc Gutenberg, ou formulaire. Déterminer le contexte d’affichage dans le navigateur: texte, attribut, URL, ou script. Tester avec des charges utiles simples, plutôt que des payloads complexes. Une simple séquence de caractères qui casse une structure (guillemet, chevron, backtick) peut suffire à révéler une injection. Vérifier le type exact du contenu au moment de l’affichage. Par exemple, un champ qui passe par un filtrage peut conserver des caractères inattendus.
Même sans outils automatisés, vous pouvez souvent isoler la vulnérabilité en quelques minutes. Le plus important est d’observer ce que le navigateur interprète réellement dans le DOM, plutôt que de conclure sur la base de la présence de caractères “bizarres” dans la réponse HTTP.

Liste d’examen rapide pour un audit local
Si vous devez rapidement cadrer un audit sur un thème ou un plugin, gardez une checklist courte. Elle aide à ne pas sauter des catégories entières.
Lister toutes les sorties echo ou print avec des variables non triviales, puis classer le contexte (HTML texte, attribut, URL, JS). Pour chaque entrée (options, meta, GET, POST, shortcode), tracer jusqu’au dernier point d’affichage. Vérifier l’usage des fonctions d’assainissement et d’échappement: s’assurer que la donnée est échappée à la sortie, pas seulement “sanitisée” à l’entrée. Chercher les constructions de chaînes HTML et JavaScript inline à partir de données dynamiques. Tester les pages publiques et l’admin séparément, car les contraintes et les utilisateurs exposés changent.Cette liste ne remplace pas un raisonnement complet, mais elle met le projecteur au bon endroit.

Réparer: corriger sans casser
Une correction XSS “propre” fait plus que “ajouter un escape”. Elle s’assure que le bon escape est choisi et que les transformations amont n’introduisent pas de faux sentiment de sécurité.
Prenons quelques exemples typiques de défauts, et comment on les corrige de façon responsable.
1) Texte inséré dans le contenu HTML
Défaut courant: une variable affichée avec echo $value; alors que $value provient de settings ou de métadonnées. La correction consiste à échaper pour du HTML texte. La fonction exacte dépend du style de rendu du projet, mais l’idée est stable: traiter la donnée comme du texte.
Le piège: si vous attendez un sous-ensemble de HTML autorisé, un échappement “plein texte” peut supprimer ce qui était attendu. Dans ce cas, vous devez décider: soit vous autorisez un HTML contrôlé, soit vous forcez le contenu en texte. La sécurité exige une décision explicite, pas un compromis implicite.
2) Attributs HTML, notamment href et src
Injection dans href ou src est un classique. Même avec un échappement HTML, vous pouvez garder un problème si l’URL contient un schéma inattendu, ou si l’attribut est mal encadré.
La correction consiste généralement à valider la valeur comme URL et à l’échapper correctement pour l’attribut. Selon votre usage, il peut être nécessaire de refuser certains schémas ou de normaliser. Le bon réglage dépend du produit. Si vous autorisez les URL externes, ne laissez pas l’attaquant décider du schéma.
3) Données injectées dans du JavaScript inline
C’est le cas le plus dangereux et souvent le plus délicat. Une variable insérée dans un script peut fermer une chaîne, ou introduire des caractères qui changent le parsing.
La correction consiste souvent à éviter le JavaScript inline avec concaténation de données, et préférer un canal de données sérialisées de façon sûre, puis un rendu côté JS qui consomme des données. WordPress offre des mécanismes natifs pour ce genre de choses, mais la logique reste la même: sérialiser correctement et ne jamais faire confiance à la donnée au niveau du parsing JS.
Le piège: remplacer un échappement mal fait par un échappement “texte” peut encore laisser un problème si le contexte reste un script. L’échappement doit être pensé pour le langage et le parsing.
4) Shortcodes et blocs
Les shortcodes et blocs WordPress sont des zones où les développeurs combinent souvent contenu utilisateur et template. Même si le shortcode est utilisé “en interne”, il peut exister des scénarios où un administrateur ou un auteur saisit du contenu, ou où une donnée provient d’options.
La correction doit respecter le contrat du shortcode: si le shortcode doit accepter du texte brut, vous échappez comme du texte. S’il accepte du contenu HTML, vous traitez avec un filtrage de l’HTML autorisé, puis vous protégez encore à l’affichage. Une fois que le contenu devient “HTML”, il faut réfléchir au contexte de rendu exact.
Où l’assainissement (sanitize) ne suffit pas
Beaucoup de projets utilisent sanitize_text_field ou sanitize_textarea_field. Cela réduit certains problèmes, mais ce n’est pas une protection universelle contre les XSS.
- L’assainissement agit sur la forme de l’entrée. L’échappement agit sur la sortie et sur le contexte.
Un code peut sanitiser une entrée, puis l’afficher dans un contexte différent ou mal échappé. Le navigateur ne s’intéresse pas à votre intention, il interprète le résultat final selon le HTML produit.
C’est pour cela que dans un audit XSS, je cherche la dernière étape d’affichage plutôt que la première étape de nettoyage. Si la sortie n’est pas échappée correctement, le correctif est presque toujours à cet endroit.
Gestion des données “à double sens”: admin, front, et utilisateurs différents
Un autre piège, c’est de corriger uniquement le front. Or, les XSS en admin peuvent être aussi dommageables, parfois plus. Un utilisateur avec accès limité peut injecter dans un paramètre, puis déclencher l’exécution pour un administrateur qui ouvrira la page.
Dans un audit, séparez mentalement:
- Les surfaces côté public (visiteur anonyme ou connecté), Les surfaces côté admin (auteurs, éditeurs, admin, super-admin), Les mécanismes d’actions asynchrones (AJAX, REST).
Les charges utiles et les scénarios diffèrent. Même un champ “admin-only” peut devenir un levier si des rôles peuvent y écrire.
Je recommande aussi de tester avec au moins deux profils de rôle, quand c’est possible dans votre environnement. Les permissions changent ce que vous verrez, mais elles changent aussi ce que l’attaquant peut injecter.
Détection et validation: au-delà du “ça marche”
Une correction XSS doit être validée par une observation concrète dans le navigateur. Vous cherchez l’effet, pas juste la présence de caractères échappés.
Après correction, vérifiez:
- que la charge utile ne se transforme pas en nœud DOM inattendu, que les guillemets et caractères spéciaux ne cassent pas vos attributs, que vos scripts ne contiennent pas de fragments injectés, que les liens affichés fonctionnent encore (sans URL “vidées” ou cassées).
C’est aussi là que les trade-offs apparaissent. Par exemple, autoriser un champ “avec HTML léger” pour un contenu marketing peut être tentant. Mais si ce contenu se retrouve ensuite dans des attributs, ou dans des contextes de script, ce qui était acceptable dans un bloc devient problématique ailleurs. Dans mes audits, je privilégie une approche consistante: soit on autorise un HTML et on le rend dans un contexte maîtrisé, soit on force le texte.
Outillage: ce qui aide, sans devenir une béquille
Je n’aime pas faire de l’outil une religion. Cela dit, certains outils peuvent accélérer la première passe sur un codebase:
- analyse statique pour repérer des concatenations et des echo de variables recherche de fonctions “suspectes” ou de templates qui manquent d’échappement revue manuelle ciblée sur les chemins d’exécution trouvés
L’important est de garder en tête que l’analyse statique peut rater des chemins dynamiques ou des filtrages. L’inverse est vrai aussi: elle peut signaler des endroits non exploités. Elle sert à orienter votre temps.
Le meilleur couple reste: repérage, puis validation dans le navigateur.
Patcher proprement: bonnes pratiques de style et de discipline
Une correction durable change la façon d’écrire le code. Si vous “corrigez au cas par cas” sans discipline, vous aurez la même dérive ailleurs dans un mois.
Dans un projet thème ou plugin, j’incite à:
- factoriser les échappements dans des fonctions utilitaires quand le contexte est récurrent documenter clairement quelles données sont supposées être “texte”, “HTML filtré”, ou “données pour script” éviter la concaténation brute de HTML dans plusieurs endroits, car elle multiplie les oublis préférer des structures où le contexte est évident (par exemple, construire des attributs en encadrant systématiquement leurs valeurs)
Ce n’est pas du formalisme. C’est une façon d’empêcher la prochaine régression.
Exemple de scénario réaliste, le genre de cas que je vois
Imaginons un plugin qui affiche un encart “message de bienvenue” paramétrable dans l’admin. Le développeur a créé un champ dans l’écran de réglages, et il enregistre la valeur en base. Ensuite, dans un template front, il affiche:
- le texte dans un paragraphe, mais aussi un lien “en savoir plus” dont l’URL est stockée dans un champ séparé, et le tout est repris dans un bloc qui initialise un petit script d’interaction.
Les développeurs ont peut-être sanitizé le texte à l’entrée, et échappé une partie. Mais si l’URL ou le message sont repris dans un script inline, ou si l’encart utilise des attributs comme data-* sans échappement de contexte, une XSS peut surgir.
Dans ce scénario, le correctif nécessite de traiter trois contextes distincts. C’est typiquement là que les équipes se trompent, parce qu’elles pensent qu’un seul “escape” global suffirait. Un audit bien mené montre exactement où la donnée change de contexte.
Plan d’action recommandé (sans lourdeur)
Vous pouvez faire un audit XSS de manière progressive, surtout si vous avez un site en production.
D’abord, choisissez le périmètre: thème actif et plugins critiques (ceux qui gèrent formulaires, contenu dynamique, paramètres, shortcodes, blocs). Ensuite, créez un environnement de test ou au moins un staging, car la vérification navigateur est indispensable.
Ensuite, passez par la logique: cartographier les chemins d’entrée vers sorties, repérer les sorties sensibles, puis corriger avec une approche de contexte. Enfin, ré-testez.
Si vous travaillez en équipe, je recommande aussi un minimum de “contrat” d’échappement: une page de repères, une revue ciblée sur les changements de templates, et une règle de base sur les données utilisées dans du script.
Cela évite que chaque développeur règle le sujet “à sa manière”, ce qui est une source directe de variations et donc de bugs.
Ce qu’il faut exiger d’un correctif
Quand une équipe annonce “XSS corrigée”, je veux voir des éléments concrets:

- un repérage du point d’affichage vulnérable, pas seulement la sanitisation d’entrée un échappement adapté au contexte (HTML texte, attribut, URL, script) une validation en navigateur avec une charge utile de test la vérification que les cas légitimes continuent à fonctionner (contenu attendu, liens, mise en forme)
Si vous n’obtenez que “on a ajouté esc_html ici”, c’est un bon réflexe, mais pas une preuve. La preuve, c’est le comportement du rendu.
Maintenir la sécurité après l’audit
Un audit n’est pas une date, c’est un processus. Les nouvelles fonctionnalités introduisent de nouveaux chemins. Un champ qui arrive par une importation, une nouvelle option admin, un nouveau shortcode, et la surface augmente.
Pour maintenir, je conseille de:
- intégrer une revue systématique des points d’affichage lors des PR garder une discipline d’échappement par contexte dans chaque nouveau template surveiller les changements de plugins et thèmes, car une mise à jour peut modifier le rendu et déplacer la zone à risque
Et si vous tenez un “registre” des points corrigés et des patterns appris, vous gagnez du temps lors des audits suivants. Les mêmes erreurs ont tendance à revenir, mais cette fois, vous les attrapez plus vite.
Si vous n’avez pas le temps: stratégie de réduction du risque
Quand la contrainte est forte, il faut prioriser. Vous pouvez réduire significativement le risque XSS sans tout recompiler.
Commencez par les points de sortie les plus sensibles:
- champs qui acceptent des valeurs riches ou qui sont utilisés dans des attributs templates qui injectent dans du JavaScript shortcodes et blocs qui affichent du contenu paramétrable pages qui exposent des paramètres GET/POST dans des vues
Puis, pour chaque point, cherchez le contexte final d’affichage et appliquez un correctif ciblé, validé dans le navigateur.
Ce n’est pas parfait, mais c’est un progrès net et mesurable. En pratique, les équipes qui réussissent sont celles qui traitent d’abord les “surfaces de sortie”, pas seulement les sources.
Où je termine, après un audit
À la fin d’un audit contre les XSS dans thèmes et plugins, ce que j’aime obtenir, ce n’est pas seulement une suite de patchs. C’est une https://gardewp.fr/securite-wordpress/ compréhension partagée: quelles données peuvent contenir des caractères inattendus, où elles finissent dans le HTML, et comment les escapers sont choisis.
WordPress offre beaucoup de briques utiles, mais la sécurité ne dépend pas d’une fonction magique. Elle dépend d’une discipline: traiter la donnée comme une donnée, puis l’échapper correctement au moment exact où elle devient du rendu.
Si vous construisez cette discipline dans votre thème et vos plugins, vous réduisez fortement la probabilité de régression. Et surtout, vous transformez un sujet anxiogène en travail structuré, audit et sécurisation WordPress, avec une méthode qui tient même quand le code évolue.