The Browser Storage Dilemma of JWT

Samarendra Kandala
Fission Labs
Published in
10 min readFeb 9, 2023

JWT is one of the key components in achieving statelessness in RESTful architecture.Handling of JWT storage on the client side (especially on the browser side) is the most under-emphasized topic in architecture design.

In this article, I will explain all about the storage options and clear the storage dilemma of JWT once for all. Before we talk about the merits and demerits of each storage type, we need to understand the common attacks related to browser storage.

You might have heard about Cross Site Scripting (XSS) attacks, the most dangerous attack on browsers.

Cross Site Scripting (XSS)

Cross Site Scripting (XSS) is the worst. Cross-Site Scripting (XSS) attacks are a type of injection, in which malicious scripts are injected into otherwise benign and trusted websites.

Don’t get scared of jargon, I will explain in plain English.

If some attacker is able to execute scripts on the end user’s browser using your web application without your knowledge then that is called an XSS attack. Your web application is vulnerable to XSS attacks.

What will happen if someone is able execute the scripts, will there be any security issues? Will there be !! That’s obvious.

E.g. If you have a bank application which is vulnerable to XSS attack, they can have access to your account, They can delete/close your account.

This is possible because the attacker can execute the malicious scripts on the user’s browser, they can steal the JWT stored in local storage, and imitate the actions/requests as done by the user which your server won’t be able to differentiate.

Let me show you a very basic and simple example of how an attacker can execute a script on your web application. Let’s say you have an input text field, when a user enters some text you need to change “this is xss message” value to some value .

Code for it.

<body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.1/jquery.min.js"></script>
<script>
function func() {
console.log("clicked")
$("#mss").append(document.getElementById("in-txt").value);
}
</script>


<div id="mss">this is xss message</input></div>
<br />
<input type="text" id="in-txt" name="query" value=""></input>
<button name="submit" type="button" onclick="func()">Submit</button>
</body>

If a user inputs this text “<script>alert(“xss”)</script>”, then it will show an alert pop up. The web application is vulnerable to XSS as it is able to execute the JavaScript.

If you need in depth details of the XSS attack check this link.

Let’s also learn about the second type of attack Cross Site Request Forgery (CSRF) attack

Cross Site Request Forgery (CSRF)

CSRF is one more type of attack we need to be aware of before choosing the right browser storage for storing JWT.

The attack is all about the user being tricked to make malicious requests in the web application in which the user is currently logged in and authenticated.

Image Credit : https://reflectoring.io/complete-guide-to-csrf/

  • The attacker sends a malicious link embedded in an image via email and lures him to click the link. The link points to the bank website requesting to transfer funds to another account.
  • If the user clicks on the image while the user is using the bank website in another tab on the browser , the link will call the bank website to perform some action and the action will be successful as the user is already logged in. This user won’t be aware of this action at all.

you might have a question on how the authentication works as the transfer funds request is triggered from email and not from the bank website.

This attack is mostly associated with cookies. All the browser checks is which domain the request is made to (in this case bank’s). If there are any cookies associated with that bank domain, it will attach them automatically ignoring from which website the request originated from.

That is why the name cross site request forgery, the request of one site is forged by the other site and still it works if you store JWT in cookies. There are ways to mitigate this, we will discuss it in the cookie section.

Summarizing it at a high level.

  • XSS is executing malicious scripts on your website.
  • CSRF is forging the request to the original(first-party) website from another evil website/link.

Let’s discuss the storage options and how these attacks are mitigated.

Storage Options

  • LocalStorage
  • Cookie

LocalStorage

It’s an HTML5 feature where the website can store any information in the user’s browser. This is very handy as it will be directly accessed by JavaScript code. LocalStorage is just a JavaScript object that the application can add and remove the data.

localStorage.setItem('myCat', 'Tom');
const cat = localStorage.getItem('myCat'); // return Tom

If you execute the setItem in the browser console and check the LocalStorage in the application section, you can see the value that was set.

localStorage.removeItem('myCat'); // remove item
localStorage.clear(); // clears the data

LocalStorage is cool. It gives complete access to JavaScript and is handled by the front-end without any server being involved. LocalStorage has at least 5MB storage across major browsers, which is way more than the cookie provided storage(4KB) and it is by default immune to CSRF attack. All these provide a compelling reason to store JWT in local storage.

Unlike cookies, If a JWT is stored in local storage, it won’t be automatically sent with every request. we need to have a logic to add it to every request. That is why it’s immune to CSRF attacks.

You may feel local storage is the best option to store JWT and most stateless JWT’s are more than 4KB anyway.

But hold your horses till the end and make a decision. As I mentioned before, local storage can be accessed with JavaScript. so any script that runs in your web application can access the local storage, which means it is vulnerable to XSS attack. If the attacker is able to run some malicious script then they can access the local storage data and also can send malicious requests. So the attacker can steal the JWT stored in local storage and our application will be compromised.

I know what you might be thinking “My application is very secure and no attacker can run JavaScript in my application. “. That’s a fair argument, if no attacker can run JavaScript on your website then your application is technically safe from XSS. But think about the scripts that you are using which are not part of your code logic.

  • Link to Bootstrap
  • Link to JQuery
  • Link to any third party lib , etc.

If any of those are compromised then you are screwed. So are cookies a safer option? Let’s discuss cookies first and then you decide.

Cookie

Cookie is a small block of data that is sent by the server to the user’s browser. The browser will store the cookie and attach the server domain(e.g. google.com), scheme (such as HTTP or HTTPS) to that cookie . A server can send more than one cookie for a domain.

With Every request sent to a server’s domain (e.g. google.com) from your browser, all the cookies which are attached to that server domain will be sent along with that request automatically.

We can check the cookie data for any website

  • Open developer tools in chrome and open the Application tab.
  • If you see the below image , you can see that the browser has stored many cookies along with the paths (such as “/”, “/person” etc.), domain and other flags to the cookies.
  • So any request that is sent to that domain (google.com) from the browser, the browser will also send all the cookies attached to that domain along with the request.
  • If there is a path field set for a cookie, then those particular cookies will be send along with the request only if the request is sent to that path/sub-paths and to that domain

The max size of cookie capped to 4KB and used for storing session data and other small blocks of data that server needs with every request.

Cookies are vulnerable to CSRF attack as the browser just sends all the cookies attached to a domain with every request made to that domain. To mitigate this risk we have to use the “SameSite” flag. When set to “strict”, the browser will not allow any cross site request which means other websites/domains won’t be able to send requests to your server domain thus eliminating the risk of CSRF altogether.

In the above bank example if the bank server sets the cookie with flag SameSite:strict, when the user clicks on the malicious link in email then the authentication cookie won’t be sent along with the request as the request is originating from a different domain than that of banks and the request will fail.

SameSite alone won’t be sufficient for the CSRF protection. The plain cookie without any flags can be stolen with an XSS attack which can be mitigated with the “httpOnly” flag. If you are using the “secure” flag, the cookie can only be transmitted using HTTPS connections (SSL/TLS encryption) and never sent in clear text.

We can also mitigate the CSRF issue with the implementation of CSRF token, but this requires a server side system and building such a system for large scale applications is a big task.

Does this mean cookies are immune to XSS and only prone to CSRF?

No, with the httpOnly flag, the JavaScript code may not be able to access the cookie and the attacker may not be able to steal the JWT. But with XSS, the attacker can still execute JavaScript in your web application which can make malicious requests to the server and cookies will be attached with each request. In the case of cookies as opposed to local storage, the attacker might not be able to steal the tokens and use it later but that won’t matter as the malicious request can be made just by executing JavaScript.

Is Cookie worse than local storage and vulnerable to both XSS and CSRF?

If your web application is vulnerable to XSS attack, no matter what storage you use, the application is compromised. There are ways to mitigate this issue and check the XSS prevention cheat sheet. XSS attacks can be heavily mitigated by using a modern JavaScript framework, paying attention to NPM audit warnings, having proper CORS configuration, and adding in Content security policy(CSP) header.

Summarizing all the options in one glance.

Conclusion:

  • Irrespective of the storage always use proper frameworks, check npm warning, add CORS access header, use CSP header, use html input sanitization techniques.
  • If you don’t have an Anti-CSRF system or JWT size is greater than 4KB, it won’t make sense to use cookies. Maybe you can divide the token into multiple parts and store but this is just a hack.
  • Using local storage will be fully vulnerable to XSS as the attacker can steal the token but if the token size is more than 4KB you don’t have any option other than using local storage.

My Suggestion:

In any practical use case you will have both accessToken and refreshToken,

refreshToken:

Solution: Store the refreshToken in a cookie with path /refreshtoken with secure, httpOnly and SameSite flags. This functionality of this API is used to get a valid accessToken (when expired) using the refresh token. As we have attached the path to the cookie, the cookie gets attached to a request sent to the path(/refreshtoken) and will not be attached to all the requests.

Benefit: No one will be able to steal the refresh token. If someone tries CSRF or XSS to send malicious requests, the attacker won’t gain anything and this will just return a new access token and nothing else. No real damage will be done to our application. If you have proper CORS headers, the attacker won’t be able to read the responses.

accessToken:

Solution: If you have an anti-CSRF system and your JWT token size is less than 4KB then store access token in cookie with secure, httpOnly and SameSite flags. It is partially vulnerable to XSS which can be mitigated by above mentioned points.
If you don’t have any anti-CSRF system then divide the JWT accessToken into two parts, attach it on the server side and validate.

  • Signature — Save in cookie with secure, httpOnly and SameSite flags
  • Header and Payload — Save in localstorage.

Benefit: If an attacker tries CSRF, as the payload and headers sections are part of local storage, they won’t be attached with the request automatically and only the signatures section will be attached. So the CSRF will always fail.
If the attacker tries to steal the token via XSS, the attacker won’t have access to the full token as the signature is stored in a cookie and won’t be able to access it via javascript. Thus is just partially vulnerable, same as the “JWT stored in a cookie with the Anti-CSRF system” without the need of any Anti-CSRF system.

--

--