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) 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:
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:
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?
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!
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:
- Sanitise anything that could come from the user (including URL parameters).
- 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:
Have fun! Thanks for reading! 😃
Top comments (1)
This article could also be named "Reflected XSS attack on cookies" since the XSS part is just the way to inject malicious JS. And once the malicious JS is injected then it could read Cookies just as easily as it could read localStorage values. Right?
Yeah, I just checked
document.cookie
in dev tools of browser on a site and I can see the entire cookie data.