Seriously. Just stop it already.
I don't know what it is, exactly, that drives so many developers to store session information in local storage, but whatever the reason: the practice needs to die out. Things are getting completely out of hand.
Almost every day I stumble across a new website storing sensitive user information in local storage and it bothers me to know that so many developers are opening themselves up to catastrophic security issues by doing so.
Let's have a heart-to-heart and talk about local storage and why you should stop using it to store session data.
What is Local Storage?
I'm sorry if I was a bit grumpy earlier. You don't deserve that! Heck, you might not even be familiar with what local storage is, let alone be using it to store your session information!
Let's start with the basics: local storage is a new feature of HTML5 that basically allows you (a web developer) to store any information you want in your user's browser using JavaScript. Simple, right?
In practice, local storage is just one big old JavaScript object that you can attach data to (or remove data from). Here's an example of some JavaScript code that stores some of my personal info in local storage, echoes it back to me, and then (optionally) removes it:
// You can store data in local storage using either syntax
localStorage.userName = "rdegges";
localStorage.setItem("favoriteColor", "black");
// Once data is in localStorage, it'll stay there forever until it is
// explicitly removed
alert(localStorage.userName + " really likes the color " + localStorage.favoriteColor + ".");
// Removing data from local storage is also pretty easy. Uncomment the lines
// below to destroy the entries
//localStorage.removeItem("userName");
//localStorage.removeItem("favoriteColor");
If you run the JavaScript code above in your browser on a test HTML page, you'll see the phrase “rdegges really likes the color black.” in an alert message. If you then open up your developer tools, you'll be able to see the that both the userName and favoriteColor variables are both stored in local storage in your browser:
Now you might be wondering if there's some way to use local storage so that the data you store is automatically deleted at some point and you don't need to manually delete every single variable you put in there. Luckily, the HTML5 working group (shout out!) has your back. They added something called sessionStorage to HTML5 which works exactly the same as local storage except that all data it stores is automatically deleted when the user closes their browser tab.
What's Cool About Local Storage?
Now that we're on the same page about what local storage is, let's talk about what makes it cool! Even though the whole point of this article is to dissuade you from using local storage to store session data, local storage still has some interesting properties.
For one thing: it's pure JavaScript! One of the annoying things about cookies (the only real alternative to local storage) is that they need to be created by a web server. Boo! Web servers are boring and complex and hard to work with.
If you're building a static site (like a single page app, for instance), using something like local storage means your web pages can run independently of any web server. They don't need any backend language or logic to store data in the browser: they can just do it as they please.
This is a pretty powerful concept and one of the main reasons that local storage is such a hit with developers.
Another neat thing about local storage is that it doesn't have as many size constraints as cookies. Local storage provides at least 5MB of data storage across all major web browsers, which is a heck of a lot more than the 4KB (maximum size) that you can store in a cookie.
This makes local storage particularly useful if you want to cache some application data in the browser for later usage. Since 4KB (the cookie max size) isn't a lot, local storage is one of your only real alternative options.
What Sucks About Local Storage
OK. We talked about the good, now let's spend a minute (or two!) talking about the bad.
Local storage is soooo basic. WHEW. I feel better already getting that off my chest. Local storage is just an incredibly basic, simple API.
I feel like most developers don't realize just how basic local storage actually is:
It can only store string data. Boo. This makes it pretty useless for storing data that's even slightly more complex than a simple string. And sure, you could serialize everything including data types into local storage, but that's an ugly hack.
It is synchronous. This means each local storage operation you run will be one-at-a-time. For complex applications this is a big no-no as it'll slow down your app's runtime.
It can't be used by web workers =/ This means that if you want to build an application that takes advantage of background processing for performance, chrome extensions, things like that: you can't use local storage at all since it isn't available to the web workers.
It still limits the size of data you can store (~5MB across all major browsers). This is a fairly low limit for people building apps that are data intensive or need to function offline.
Any JavaScript code on your page can access local storage: it has no data protection whatsoever. This is the big one for security reasons (as well as my number one pet peeve in recent years).
To keep it short, here's the only situation in which you should use local storage: when you need to store some publicly available information that is not at all sensitive, doesn't need to be used in a high-performance app, isn't larger than 5MB, and consists of purely string data.
If the app you're using doesn't fit the above description: don't use local storage. Use something else (more on this later).
Why Local Storage is Insecure and You Shouldn't Use it to Store Sensitive Data
Here's the deal: most of the bad things about local storage aren't all that important. You can still get away with using it but you'll just have a slightly slower app and minor developer annoyance. But security is different. The security model of local storage IS really important to know and understand since it will dramatically affect your website in ways you may not realize.
And the thing about local storage is that it is not secure! Not at all! Everyone who uses local storage to store sensitive information such as session data, user details, credit card info (even temporarily!) and anything else you wouldn't want publicly posted to Facebook is doing it wrong.
Local storage wasn't designed to be used as a secure storage mechanism in a browser. It was designed to be a simple string only key/value store that developers could use to build slightly more complex single page apps. That's it.
What's the most dangerous thing in the entire world? That's right! JavaScript.
Think about it like this: when you store sensitive information in local storage, you're essentially using the most dangerous thing in the world to store your most sensitive information in the worst vault ever created: not the best idea.
What the problem really boils down to is cross-site scripting attacks (XSS). I won't bore you with a full explanation of XSS, but here's the high level:
If an attacker can run JavaScript on your website, they can retrieve all the data you've stored in local storage and send it off to their own domain. This means anything sensitive you've got in local storage (like a user's session data) can be compromised.
Now, you might be thinking “So what? My website is secure. No attacker can run JavaScript on my website.”
And that's a reasonable point. If your website is truly secure and no attacker can run JavaScript code on your website then you are technically safe, but in reality that is incredibly hard to achieve. Let me explain.
If your website contains any third party JavaScript code included from a source outside your domain:
- Links to bootstrap
- Links to jQuery
- Links to Vue, React, Angular, etc.
- Links to any ad network code
- Links to Google Analytics
- Links to any tracking code
Then you are currently at risk for having an attacker run JavaScript on your website. Let's say your website has the following script tag embedded inside it:
<script src="https://awesomejslibrary.com/minified.js"></script>
In this case, if awesomejslibrary.com is compromised and their minified.js script gets altered to:
- Loop through all data in local storage
- Send it to an API built to collect stolen information
... then you are completely screwed. In this situation the attacker would have easily been able to compromise anything you had stored in local storage and you would never notice. Not ideal.
As engineers, I think we're frequently susceptible to thinking that we would never embed third-party JavaScript in our websites. But in the real world, this scenario rarely plays out.
At most companies, the marketing team directly manages the public website using different WYSIWYG editors and tooling. Can you really be sure that nowhere on your site are you using third-party JavaScript? I'd argue “no”.
So to err on the side of caution and dramatically reduce your risk for a security incident: don't store anything sensitive in local storage.
PSA: Don't Store JSON Web Tokens in Local Storage
While I feel like I made myself clear that you should never ever store sensitive information in local storage in the previous section, I feel the need to specifically call out JSON Web Tokens (JWTs).
The biggest security offenders I see today are those of us who store JWTs (session data) in local storage. Many people don't realize that JWTs are essentially the same thing as a username/password.
If an attacker can get a copy of your JWT, they can make requests to the website on your behalf and you will never know. Treat your JWTs like you would a credit card number or password: don't ever store them in local storage.
There are thousands of tutorials, YouTube videos, and even programming classes at universities and coding boot camps incorrectly teaching new developers to store JWTs in local storage as an authentication mechanism. THIS INFORMATION IS WRONG. If you see someone telling you to do this, run away!
What to Use Instead of Local Storage
So with all of local storage's shortcomings, what should you use instead? Let's explore the alternatives!
Sensitive Data
If you need to store sensitive data, you should always use a server-side session. Sensitive data includes:
- User IDs
- Session IDs
- JWTs
- Personal information
- Credit card information
- API keys
- And anything else you wouldn't want to publicly share on Facebook
If you need to store sensitive data, here's how to do it:
When a user logs into your website, create a session identifier for them and store it in a cryptographically signed cookie. If you're using a web framework, look up “how to create a user session using cookies” and follow that guide.
Make sure that whatever cookie library your web framework uses is setting the
httpOnlycookie flag. This flag makes it impossible for a browser to read any cookies, which is required in order to safely use server-side sessions with cookies. Read Jeff Atwood's article for more information. He's the man.Make sure that your cookie library also sets the
SameSite=strictcookie flag (to prevent CSRF attacks), as well as thesecure=trueflag (to ensure cookies can only be set over an encrypted connection).Each time a user makes a request to your site, use their session ID (extracted from the cookie they send to you) to retrieve their account details from either a database or a cache (depending on how large your website is)
Once you have the user's account info pulled up and verified, feel free to pull any associated sensitive data along with it
This pattern is simple, straightforward, and most importantly: secure. And yes, you can most definitely scale up a large website using this pattern. Don't tell me that JWTs are “stateless” and “fast” and you have to use local storage to store them: you're wrong!
Non-String Data
If you need to store data in the browser that isn't sensitive and isn't purely string data, the best option for you is IndexedDB. It's an API that lets you work with a database-esque object store in the browser.
What's great about IndexedDB is that you can use it to store typed information: integers, floats, etc. You can also define primary keys, handle indexing, and create transactions to prevent data integrity issues.
A great tutorial for learning about (and using) IndexedDB is this Google tutorial.
Offline Data
If you need your app to run offline, your best option is to use a combination of IndexedDB (above) along with the Cache API (which is a part of Service Workers).
The Cache API allows you to cache network resources that your app needs to load.
A great tutorial for learning about (and using) the Cache API is this Google tutorial.
Please Stop Using Local Storage
Now that we've had a chance to talk about local storage, I hope you understand why you (probably) shouldn't be using it.
Unless you need to store publicly available information that:
- Is not at all sensitive
- Doesn't need to be used in an ultra high performance app
- Isn't larger than 5MB
- Consists of purely string data
... don't use local storage! Use the right tool for the job.
And please, please, whatever you do, do not store session information (like JSON Web Tokens) in local storage. This is a very bad idea and will open you up to an extremely wide array of attacks that could absolutely cripple your users.
Have a question? Shoot me an email.
Stay safe out there =)
NOTE: For those of you who made it this far who are wondering why I didn't specifically call out Content Security Policy as a way to mitigate the effects of XSS, I specifically chose not to include this because it cannot help in the situation I described above. Even if you use CSP to whitelist all third-party JavaScript domains, that does nothing to prevent XSS if the third party provider is compromised.
And while we're at it: subresource integrity (while cool) is also not a global solution to this issue. For most marketing tools, ad networks, etc. (which are by far the most commonly used types of third-party JavaScript), subresource integrity is almost never used as the providers of those scripts want to change them frequently so they can silently update functionality for their users.
UPDATE: I'm not the only one who thinks you should never store anything sensitive in local storage. So does OWASP:
... In other words, any authentication your application requires can be bypassed by a user with local privileges to the machine on which the data is stored. Therefore, it's recommended not to store any sensitive information in local storage.








Oldest comments (198)
Thanks for introducing me to IndexedDB. I had been using localStorage in a webapp and IDB sounds like a much better solution.
IDB is great for some use cases =)
Now, I want to know, how BaaS handles these.
Yep, it seems to me that people started working with microframeworks on nodejs etc. thinking they understood what they were doing, then realized they're missing all the convenient features they used to have and didn't have a clue of how they were implemented so they just picked some things that sounded cool (JWT & localStorage) and went with those without even a single second of thought as to "why isn't everyone doing this?" or similar.
Secure information, such as session information, should almost universally be stored in
httpOnly&securecookies, potentially withSameSite=strict. I just don't see any reason for JS to touch the cookies in general, just use a/meendpoint or similar to check if you're logged in and to fetch your user's information so you don't need to touch the secure cookies.The
/meendpoint solution doesn't work with a stateless API.I don't really understand what your thinking of stateless is here. If your cookie holds all the necessary data, e.g. your user information in encrypted form or JWT style tokens or similar, then your backend server really needs to know no other "state".
To be clear, you're suggesting using cookies for authentication in a REST API? In my experience this is not a successful pattern and certainly doesn't conform to some of the core principles of REST.
It does conform to REST. There's nothing not RESTFUL about cookies.
Cookies are purely an HTTP header. When a cookie is sent from a browser->server it is sent via the
Cookie: name=valueformat. This is it.What you're comparing it to is the Authorization header, which looks like this:
Authorization: .... There is NO difference whatsoever here except for the name of the header.This is a really common misconception. I've started replying to comments (this article really blew up while I've been busy working...) but I plan to write a more in-depth article covering some of the arguments I only briefly mentioned here in more depth.
On top of that, not all APIs have to be RESTful and assumed to be accessed by 3rd parties.
If it's your own application, you can still make pretty RESTful APIs, using cookies, and not have to worry about the difference between using
Authorization: ...headers vs.Cookie: ...headers.Authorization:header is great when you assume the client is a non-browser application, and you can cater to those easily with your middleware. For your own browser applications cookies are better.Keep in mind that some sandbox environments don't allow arbitrary headers to be set or even cookies to be added, but they do support the RFC for Authorization headers. So, it may seem like the same thing, but when your local API (e.g. WkWebView had (has?) this as a limitation) - then you're going to have to use it that way.
So what you're trying to say is that since some systems are limited, it's ok to use a bad practice everywhere?
Of course you're allowed to work with the limitations of your system (though finding a less idiotic system is a better choice), but that doesn't mean you should use the bad practice from the bad system when working with good systems or systems you have control over and can fix.
Good article, but the language is too strong. You made a few good points but also some arguments are controversial.
I am not sure if using authentication cookie for API is a common pattern out there. I haven't seen many APIs using Cookie for authentication. Cookie and Authorisation are designed for different purposes. Yes, they are just different names behind the scene but applications treat them differently. Authorisation header is not automatically pre-filled by browsers while cookie is. That means using Cookie as authentication you're prone to CRSF. That's why we use Authorisation header to avoid CRSF as much as possible.
And last point, no one puts a few MB of data in a JWT token.
I hope nobody is putting a few MB of data in my localstorage either. If you use Authorization header, the token can be extracted by XSS etc. and sent to malicious servers and they can then use it however they wish.
If you use a
secure; httpOnlycookie it can't be stolen by malicious JS and is bound to your browser's security model. Add to that proper CSRF tokens, and e.g.sameSite=strictand you've got a decent system.Can you clarify what you mean by a "stateless" API? If I'm reading you right, the concept seems kind of nonsensical to me. I'm going to respond to what I think you're saying, but I'm worried that I'm constructing a strawman in doing so.
I see three worlds in which a
/meendpoint wouldn't be "stateless." The first is a world in which the current user is considered a form of state. Whether that's true in some absolute sense is an abstract philosophical question. The more practical question: if we're considering tracking the current user (ie "performing authentication") to be antithetical to a stateless API, then how are we supposed to then perform authorization? Is everyone just supposed to be allowed to do/see everything?The second: if you're using a
/meendpoint to retrieve user data, in lieu of retrieving it from a cookie, presumably some earlier interaction has logged the user in. I suppose there's implicit state in the fact that those two interactions must occur in order. But even if we're using bearer token auth, that token needs to have been generated somehow. Complaining about the presence of that state doesn't seem very practical to me.The third: cookies are inherently stateful because they require session data to be present on the server. This is kind of true? But if we want to use a bearer token instead, presumably we need to have generated it. We can't generate secure bearer tokens idempotently or reversibly, so we need to store some kind of data about the bearer token on the server... which to me is not philosophically all that different from creating a session?
Anyway, again, I feel like I may well be arguing against a strawman here! If there's some fourth definition I haven't considered, I'd love to hear how you're defining stateless APIs and why you feel that that definition produces more useful/usable API designs.
Not exactly true. Many cookies use signatures or encryption to make the data trustworthy for the server, so there does not need to be the somewhat traditional "sessions" list in a database.
Cool, I didn't know that! How does credential revocation work in that context? Or do you just maintain a really short validity window?
Well, it doesn't necessarily work. One way is to keep short validity, another is to tie it to e.g. the last update of the relevant user's password, or similar - you wouldn't have to check the contents of the package, just that it's been signed after the password has last changed.
There are lots of little options you could use, but it's not always necessary.
I think lietu has just described JWT in a cookie ...
Why do you assume I have access to a an endpoint where I can fetch this user's information? This user's claims could come from a trusted 3rd-party, and their claims are verified on my end by their pubkey.
I've found one of the best uses for (local|session)Storage is just to cache data. If you have a data set that is going to remain static for the entire application session but still needs to be fetched asynchronously, sessionStorage is great for caching it (unless it somehow contains sensitive data of course...).
Exactly! I love it for that. This is what I use it for as well.
Well… that was eye opening!
Good article and good awareness. However....
Quote: "If an attacker can run JavaScript on your website, they can retrieve all the data you've stored in local storage and send it off to their own domain."
Edit: "If an attacker can run JavaScript on your website, you're ka-pooched, and a local jwt is the least of your worries."
😂😂😂
"If an attacker can run JavaScript on your website, you're ka-pooched, and a local jwt is the least of your worries."
Have you ever used jQuery or any other 3rd party JS library on your site? How certain are you that that 3rd party does nothing malicious? How certain are you that 3rd party library hasn't been compromised?
Not even the library: if you're using one, you're probably pulling from a CDN right? How certain are you that CDN hasn't been compromised to serve a different version of the library than what you think you're getting?
"How certain are you that that 3rd party does nothing malicious?"
I'm not very certain. But you're missing the point. If the library is tainted, what can it do? Much, much worse than the worst case scenario here. That's the point. It's like duct tape to fix the titanic. It's like humans in a Marvel movie. Useless.
Just like Xavier, a compromised lib can mind-control the poop out of your app and wreak havoc for your users, and there's nothing you can do about it. XSS is a security rabbit hole.
When you understand what a tainted lib can do, you realize how trivial this article's point is. That is my only point.
Part of the problem with the proposed solution in this article is that cookies are tied to a specific domain. That makes it much harder and clunky to use third party security services like Auth0. Also, scalability does become a MUCH harder issue with server-side sessions; I don't care what this article says.
Best idea to overcome your tainted paranoia is to use script's
integrityattribute, eg:Also, careful with IndexDB and IE compatibility.
caniuse.com/#search=IndexedDB
Well, you stated that using Local Storage for sensitive information is bad, and that's OK. But just saying "do not use" doesn't constitute good advice. There are good and bad choices, as always. Know your choices and act accordingly.
Totally! I agree.
I like to use github.com/localForage/localForage because it uses IndexedDB or WebSQL but it falls back to local storage when needed. Still, security advice holds.
This looks awesome. Thanks for sharing Alex.
As someone who is taking their very first steps into web development, I found this article was very informative and I like the repetitive way the fact is drummed into the reader. I know that when I get to that stage in my learning, this is something that will come to mind straight away. Thanks :)
Heyo! I can tell you obviously aren't a fan of the article.
One thing I would say though: just check out the point of the article. I'm not telling people to do anything insane. I'm trying to encourage people to simplify their apps, their code, and often their security strategy by simply using cookies.
They can both contain the exact same info, transmit the same data (in the same way! via http headers), and provide very little differences in terms of implementation/API.
What I'm suggesting here is simpler, quicker, and more secure than what I've described as the problem.
And... I'm not the only one saying this stuff. OWASP, Thomas Ptacek (a well-known security researcher), and many other smart people constantly try to dissuade people from doing the things I mention above. This isn't anything obscure that I'm talking about.
Some comments may only be visible to logged-in visitors. Sign in to view all comments.