Reflected XSS attack on localStorage
Tags: security, webdev, javascript
Published: 2020-07-30 16:18:22

We've all seen the debates about localStorage vs. cookies when it comes to handling client-side JWTs. You may choose to store your JWTs in one or the other depending on which article you read. **But what does an XSS attack actually look like?**

# XSS Overview
The [Open Web Application Security Project (OWASP)](https://owasp.org/www-community/attacks/xss/) defines XSS as: 
> Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted websites.

In other words, attackers can use the features of your site to inject malicious Javascript. **It's important to note that any client-side Javascript has access to** `localStorage`, `sessionStorage` **and** `cookies` **(non-HttpOnly)**.

# Example
I'm going to use a simple error page that users are redirected to if they encounter an general error. I've seen this used many times (hopefully a little better than what I'm about to show!)

**Note:** Let's assume that our site authenticates users via JWT and stores them in `localStorage`.

Here's our beautiful error page:

![Error page example](https://dev-to-uploads.s3.amazonaws.com/i/akc5heit56nnwvtmt6gq.jpg)

It accepts `code` and `message` parameters to display in the page like so:
```
.../error.html?code=500&message=Something%20went%20wrong
```
The code that handles displaying the message looks like:
```javascript
const params = new URL(document.location).searchParams
const errorCode = parseInt(params.get("code"))
const errorMessage = params.get("message")

document.getElementById("error-code").innerHTML ="Error code: " + errorCode
document.getElementById("error-message").innerHTML = errorMessage
```
*Can you spot the mistake?* 😏

We are getting the error message from the URL and placing it into our document HTML... 🤔

What would happen if an attacker was to try to inject some Javascript instead of a message?

![XSS Success](https://dev-to-uploads.s3.amazonaws.com/i/1x8h1rm1bes1yiknotn4.jpg)

*Uh-oh!* This confirms to the attacker that this page is vulnerable to an attack called **Reflected XSS.**

With some creativity, it's not a huge leap to get the contents of your local storage (which includes your JWT) and send it off to the attacker... *bye bye token!*

![bye bye token](https://dev-to-uploads.s3.amazonaws.com/i/o6fys7svkstycp6k95hx.jpg)

Once the attacker has your token, it's trivial to reveal all information stored in that token. *They are just base64 encoded objects.*

# Solution
The main issue with our code is that we are getting the message string from the URL and inserting it directly into our document HTML. Instead, we should:
1. Sanitise anything that could come from the user (including URL parameters).
2. Use `.textContent` instead.

A good tip is **Don't store anything in the JWT you wouldn't already consider public**. This way, even if your site happens to be vulnerable to XSS, the attacker isn't gaining any private information.

# Conclusion
There is nothing wrong with storing JWTs in `localStorage`. The issue is with poor coding practices that have the potential to expose your site and users to attack. 

Granted, this was a simple (and contrived) example of reflected XSS, but there are other DOM-based attacks your app may be vulnerable to. 

It's fun to break things you're working on and see if you can patch any vulnerabilities *before* they make it out!

Here are some good places to learn more:
- [DOM-based XSS Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/DOM_based_XSS_Prevention_Cheat_Sheet.html)
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)

Have fun! Thanks for reading! 😃