Arkendia Classrooms
Leçon 14 sur 28 · 16 min

XSS stocké et XSS basé sur le DOM

Dans le XSS réfléchi, une victime doit cliquer sur un lien préparé. Imaginez maintenant une faille où la victime n’a rien à faire : elle visite simplement une page normale, et la donnée piégée l’attend déjà. C’est le XSS stocké. Et il existe une troisième variante, où le serveur n’est même pas impliqué : le XSS basé sur le DOM. Ces deux formes sont souvent plus graves, et c’est pourquoi un auditeur les cherche systématiquement.

Rappel légal et éthique

Un XSS stocké touche tous les visiteurs d’une page, donc potentiellement des personnes réelles. Sur un site réel, déposer une donnée piégée sans autorisation écrite est une infraction (articles 323-1 à 323-3-1 du Code pénal), et un acte dommageable pour des tiers. Dans votre laboratoire, un dépôt de test est sans risque : c’est la seule situation où vous le faites. Dans un audit autorisé, on utilise une preuve inoffensive et on la supprime ensuite.

Le XSS stocké

Dans un XSS stocké (stored ou persistant), la donnée non neutralisée est d’abord enregistrée par l’application (en base, dans un fichier), puis affichée plus tard à d’autres utilisateurs. Pensez à un livre d’or, un commentaire sous un article, un profil public, un nom d’utilisateur, un message dans une messagerie interne, un ticket de support lu par un opérateur.

Analogie : au lieu de remettre en main propre un faux avis à une personne (réfléchi), on l’affiche durablement sur le tableau d’affichage où tout le monde passe. Chaque visiteur le voit sans avoir cliqué sur quoi que ce soit.

Pourquoi c’est souvent plus grave

  • Aucune interaction particulière de la victime n’est nécessaire.
  • La portée est large : tous les utilisateurs qui consultent la page.
  • Les victimes peuvent être privilégiées : si la donnée est lue par un administrateur, le code s’exécute avec ses droits.
  • La persistance : tant que la donnée n’est pas supprimée, la faille continue d’agir.

Démarche de test en laboratoire

Dans le module « XSS stocké » de DVWA (livre d’or), niveau bas :

  1. Observer : poster un message normal et constater où et comment il s’affiche.
  2. Tester la neutralisation : poster un message contenant un simple élément de mise en forme comme du texte en gras. S’il s’affiche en gras, le HTML est interprété ; s’il s’affiche avec ses chevrons visibles, il est neutralisé.
  3. Prouver l’exécution avec la boîte de message de test, uniquement dans le laboratoire.
  4. Nettoyer : supprimer l’entrée de test (ou réinitialiser la base de l’application d’entraînement). En audit réel, c’est une règle de déontologie : on retire ses données de test.
  5. Consigner : où la donnée a été saisie, où elle est affichée (ces deux endroits peuvent être très différents), qui est exposé.

Un point important : le lieu de saisie et le lieu d’affichage ne sont pas toujours la même page. Un champ « nom » d’un profil peut s’afficher dans une liste d’administration, dans un e-mail, dans un export. Chaque lieu d’affichage doit être examiné, car chacun a son contexte et son éventuelle protection.

Le XSS basé sur le DOM

Le DOM (Document Object Model) est la représentation de la page que le navigateur construit en mémoire, et que les scripts de la page manipulent. Dans un XSS basé sur le DOM, la faille n’est pas dans la réponse du serveur, mais dans le script de la page elle-même : il prend une donnée contrôlable par l’utilisateur et l’écrit dans la page de façon dangereuse.

Deux notions pour s’y retrouver :

  • Une source : un endroit d’où le script lit une donnée que l’on peut influencer (la partie de l’adresse après le #, les paramètres d’URL, le stockage local, un message reçu d’une autre fenêtre).
  • Un puits (sink) : une fonction qui interprète la donnée comme du HTML ou du code (par exemple l’affectation à la propriété qui insère du HTML, ou une fonction qui évalue du texte).

Voici un exemple pédagogique de code vulnérable, côté navigateur :

// Le script lit une valeur dans l'adresse...
var nom = new URLSearchParams(location.search).get('nom');
// ...et l'insère comme du HTML dans la page
document.getElementById('accueil').innerHTML = 'Bonjour ' + nom;

Le serveur renvoie toujours la même page ; c’est le script qui introduit la faille. Le code correct utilise une propriété qui insère du texte, jamais du HTML :

document.getElementById('accueil').textContent = 'Bonjour ' + nom;

La différence entre innerHTML et textContent est l’une des plus importantes à retenir pour les développeurs web. La documentation de référence, sur developer.mozilla.org, la détaille.

Comment le repérer

Comme le serveur ne réfléchit rien, le code source de la page ne montre pas la donnée ; c’est l’inspecteur du navigateur, qui montre le DOM après exécution, qui permet de voir l’effet. La démarche :

  1. Lire les scripts de la page et repérer les sources et les puits.
  2. Chercher un chemin de la source vers le puits sans nettoyage intermédiaire.
  3. Tester, en laboratoire, avec une valeur inoffensive, et observer le DOM avec l’inspecteur.

Les modules de DOM XSS des laboratoires de la PortSwigger Academy et certains défis de Juice Shop sont conçus pour s’entraîner exactement à cela.

Comparer les trois familles

RéfléchiStockéBasé sur le DOM
Où est la faille ?Serveur, réponse immédiateServeur, donnée enregistréeScript du navigateur
Interaction de la victimeDoit ouvrir un lienVisite normale de la pageDoit ouvrir un lien ou une page
PortéeUn utilisateur à la foisTous les lecteurs de la donnéeUn utilisateur à la fois
PersistanceNonOui, jusqu’à suppressionNon
Où la corriger ?Encodage en sortie côté serveurEncodage en sortie à chaque affichageCode du script : éviter les puits dangereux

Défense : un ensemble cohérent

  • Encoder à chaque affichage, selon le contexte, y compris pour des données « de confiance » issues de la base : on encode à la sortie, pas seulement à l’entrée.
  • Côté script : privilégier les propriétés qui traitent du texte, éviter les fonctions qui interprètent du code ; si l’on doit insérer du HTML venant de l’utilisateur, le faire passer par une bibliothèque d’assainissement éprouvée.
  • CSP restrictive pour limiter l’exécution de scripts non prévus (leçon 4).
  • Cookies HttpOnly pour limiter ce qu’un script injecté peut voler.
  • Revue des zones à forte exposition : commentaires, profils, messageries, zones lues par des administrateurs.

Astuce

Quand un champ accepte du texte enrichi (gras, listes), l’application doit autoriser une petite liste blanche de balises sûres, jamais « tout sauf quelques balises ». Une liste noire de balises dangereuses est une promesse d’échec.

À vous de jouer

  1. Dans le module XSS stocké de DVWA (niveau bas), postez un message avec du texte en gras, puis une preuve de type boîte de message. Rechargez la page : que se passe-t-il à chaque visite ?
  2. Réinitialisez les données de l’application et notez comment vous avez nettoyé.
  3. Relisez l’exemple de script vulnérable de cette leçon : nommez la source, le puits, et écrivez la correction.
  4. Rédigez le constat du XSS stocké dans votre carnet, en précisant où la donnée est saisie, où elle est affichée et qui peut être exposé.
Voir une solution

Après le dépôt, la boîte de message apparaît à chaque rechargement de la page : c’est la persistance. Source : location.search (paramètre nom) ; puits : l’affectation à innerHTML ; correction : utiliser textContent. Constat type : « XSS stocké dans le livre d’or. Saisie : champ message ; affichage : page livre d’or, visible de tous les visiteurs. Impact : exécution de code chez chaque lecteur, y compris un éventuel administrateur. Gravité élevée. Correction : encoder à l’affichage, autoriser seulement une liste blanche de balises si le texte enrichi est nécessaire, CSP, cookies HttpOnly. Donnée de test supprimée. »

À retenir

  • XSS stocké : la donnée est enregistrée puis affichée aux autres ; aucune action de la victime n’est requise, la portée est large.
  • Lieu de saisie et lieu d’affichage peuvent différer : testez chaque affichage.
  • XSS basé sur le DOM : la faille est dans le script de la page (source vers puits), pas dans la réponse du serveur.
  • Préférer textContent à innerHTML pour insérer des données.
  • Nettoyez toujours vos données de test après une preuve.

Après les scripts, voyons une autre faille côté client qui exploite non pas du code injecté, mais la confiance du serveur envers le navigateur : le CSRF.

Publicité
Connectez-vous pour enregistrer votre progression. Se connecter