One thing to keep in mind is whether the JWTs are:
First-party (ie. simply for accessing your own server commands)
Third-party (ie. a JWT for Google, Facebook, Twitter, etc.)
If the JWT is first-party:
Then it doesn't matter that much whether you store the JWT in local storage, or a secured cookie (ie. HttpOnly, SameSite=strict, and secure) [assuming your site is already using HTTPS, which it should].
Now while the XSS-attack situation is arguable "game over" either way (whether local-storage or secured cookie), cookies are still a little better, because the attacker is only able to execute the attacks if/when the user has the website open in their browser.
This causes the following "annoyances" for the attacker:
"My XSS injection worked! Okay, time to collect private data on my boss and use it as blackmail. Dang it! He only ever logs in while I'm here at work. I'll have to prepare all my code ahead of time, and have it run within the three minutes he's on there, rather than getting to poke around into his data on the platform in a more gradual/exploratory way."
"My XSS injection worked! Now I can change the code to send all Bitcoin transfers to me instead! I don't have any particular target in mind, so I don't need to wait for anyone. Man though, I wish I could access the JWT token itself -- that way I could silently collect them all, then empty everyone's wallets all at once. With these cookie-protected JWTs, I may only be able to hijack a few dozen visitors before the devs find out and suspend transfers..."
"My XSS injection worked! This'll give me access to even the data that only the admins can see. Hmmm, unfortunately I have to do everything through the user's browser. I'm not sure there's a realistic way for me to download those 3gb files using this; I start the download, but there are memory issues, and the user always closes the site before it's done! Also, I'm concerned that client-side retransfers of this size might get detected by someone."
If the JWT is third-party:
In this case, it really depends on what the third-party JWTs allow the holder to do.
If all they do is let someone "access basic profile information" on each user, then it's not that bad if attackers can access it; some emails may leak, but the attacker could probably get that anyway by navigating to the user's "account page" where that data is shown in the UI. (having the JWT token just lets them avoid the "annoyances" listed in the previous section)
If, instead, the third-party JWTs let you do more substantial things -- such as have full access to their cloud-storage data, send out messages on third-party platforms, read private messages on third-party platforms, etc, then having access to the JWTs is indeed substantially worse than just being able to "send authenticated commands".
This is because, when the attacker can't access the actual JWT, they have to route all commands through your 1st-party server. This has the following advantages:
Limited commands: Because all the commands are going through your server, attackers can only execute the subset of commands that your server was built to handle. For example, if your server only ever reads/writes from a specific folder in a user's cloud storage, then the attacker has the same limitation.
Easier detection: Because all the commands are going through your server, you may be able to notice (through logs, sudden uptick in commands, etc.) that someone has developed an XSS attack. This lets you potentially patch it more quickly. (if they had the JWTs themselves, they could silently be making calls to the 3rd-party platforms, without having to contact your servers at all)
More ways to identify the attacker: Because the commands are going through your server, you know exactly when the commands are being made, and what ip-address is being used to make them. In some cases, this could help you identify who is doing the attacks. The ip-address is the most obvious way, though admittedly most attackers capable of XSS attacks would be aware enough to use a proxy.
A more advanced identification approach might be to, say, have a special message pop up that is unique for each user (or, at least, split into buckets), of such a nature that the attacker (when he loads up the website from his own account) will see that message, and try to run a new command based on it. For example, you could link to a "fake developer blog post" talking about some "new API" you're introducing, which allows users to access even more of their private data; the sneaky part is that the URL for that "new API" is different per user viewing the blog post, such that when the API is attempted to be used (against the victim), you know exactly who did it. Of course, this relies on the idea that the attacker has a "real account" on the site alongside the victim, and could be tempted/fooled by this sort of approach (eg. it won't work if the attacker knows you're onto him), but it's an example of things you can do when you can intercept all authenticated commands.
More flexible controlling: Lets say that you've just discovered that someone deployed an XSS attack on your site.
If the attackers have the 3rd-party JWTs themselves, your options are limited: you have to globally disable/reset your OAuth/JWT configuration for all 3rd-party platforms. This causes serious disruption while you try to figure out the source of the XSS attack, as no one is able to access anything from those 3rd-party platforms. (including your own server, since the JWT tokens it may have stored are now invalid)
If the JWT tokens are instead protected in http-only cookies, you have more options: You can simply modify your server to "filter out" any reads/writes that are potentially dangerous. In some cases added this "filtering" is a quick and easy process, allowing your site to continue in "read-only"/"limited" mode without disrupting everything; in other cases, things may be complex enough that it's not worth trusting the filter code for security. The point though is that you have more options.
For example, maybe you don't know for sure that someone has deployed an XSS attack, but you suspect it. In this case, you may not want to invalidate the JWT tokens of every user (including those your server is using in the background) simply on the suspicion of an XSS attack (it depends on your suspicion level). Instead, you can just "make things read-only for a while" while you look into the issue more closely. If it turns out nothing is wrong, you can just flip a switch and re-enable writes, without everyone having to log back in and such.
Anyway, because of these four benefits, I've decided to always store third-party JWTs in "secured cookies" rather than local storage. While currently the third-party JWTs have very limited scopes (such that it's not so big a deal if they are stolen), it's good future-proofing to do this, in case I'd like my app to request access to more privileged functionalities in the future (eg. access to the user's cloud storage).
* Be the first to Make Comment