← All talks

JWTs And Why They Suck

BSides London · 202214:37661 viewsPublished 2022-01Watch on YouTube ↗
Speakers
Tags
About this talk
Rory M from Trail of Bits examines critical flaws in JSON Web Tokens as session authentication mechanisms, including token revocation issues, algorithm confusion vulnerabilities, and unnecessary complexity. The talk advocates for simpler, well-tested alternatives like cookie-based sessions with random identifiers.
Show original YouTube description
Slides: https://github.com/trailofbits/publications/tree/master/presentations/JWTs,%20and%20why%20they%20suck
Show transcript [en]

All right, hello. I'm here today as kindly introduced to talk about JWTs and why they are generally terrible. First bit about who I am, so you know who you're getting your under-qualified opinions from. I do app sec at Trail of Bits, in case you didn't notice the header slide. I'm from a background of development to devops, dev sec ops, whatever it is now, to security engineering, web app pen testing, blah blah blah. So, I've seen, written, and audited a lot of very bad code. Uh opinions in this talk are mine, not my company's, blah blah blah. And I'm glad I managed to get the arrow right. That was about 50/50, so. For those of you don't know, JWTs are

JSON Web Tokens. They are, for reasons indecipherable, quite popular to use as session tokens these days. Now, the term JWT actually refers to multiple things. Um JWS and JWE, which are JSON Web Signatures and JSON Web Encryption Standards. We're talking about the former, web signatures. Um better yet, the header that applies to both of those is known as JOSE, the JSON Object Signing Web Encryption Standards, which uses algs defined in the JWA, the JSON Web Acronym Standards, because acronyms. JWTs are a tech stack that are pushed by a couple of um providers I've been told not to name because they make authentication as a service that work with them, and for lack of any other

reason. In this talk, I'm going to spend the next 13 or so minutes talking about why they suck as authentication tokens, why you shouldn't use them. They're also apparently pronounced "jot" for those of you who didn't know, which I will not be doing. To briefly summarize why I'm doing this talk, I've seen I've seen a lot of JWTs in the wild lately um both during work and just during personal time. Um but I don't really think there's a lot of great resource around why they're not really a great technology to use and some of the sharp edges involved in actually using them. So, this talk's going to build on and helping bring attention to a lot of great resources

I've had and read online from people much smarter than I am. I'll reference later on. I'm also hoping to make any developers in the audience, if there are any of you here, to question whether you actually need to use JWTs and convince you you don't, and then suggest some better alternatives. So, what is a JSON Web Token? A JWT is three base64-encoded blobs of JSON uh joined together by full stops. Uh you might have seen it in your own wanderings and realized uh the easy telltale sign is that they start with um e j h y blah blah blah, because that's what start of a JSON blob looks like in base64. That bit in red is known as the JOSE

header. So, the JWT is three of these fields: the header in red, the claims in blue, and the signature in green. The middle field is the claims, and you can technically dump any JSON blob you want into the claims field, but it usually has at least an expiry time and some sort of user ID. Now, unfortunately, because you can put anything into the claims, a lot of people use this to try and use JWTs as a communication format between their different backends to carry around all their state on the client, which doesn't sound at all nightmarish. And the final blob, as mentioned in green, is a signature. So, the idea is when you issue one of these tokens from

your backend, you dump a load of data into the middle JSON blob, and then you sign it, so you can just trust, if you can validate the signature on that blob that you've received, that that is a valid token that you've issued containing legit data. So, I will now attempt to discuss all 99 problems I have with JWTs in the next 10 minutes, so I might talk a bit faster. Problem number one. A short bit of background. One of the big selling points, as touched on, is that JWTs are stateless. That's one of the big appeals behind the technology. So, each token because each token is actually signed, you don't need to round trip to a database to actually check if

a uh for example, a user session token is actually representing a valid user session. You just check that that has been signed by this key, and that's valid data. So, you can skip out round tripping to the database every time you want to authenticate users, and you can infinitely scale, which is amazing. Haha, except no, it's not. Because the tokens are stateless, it means that once a token is issued, there's actually no means to revoke it at all. Uh nothing built into the standard that allows you to revoke a token until the token expires. This means that if you've issued a token, you have a need to ban a user, log out a compromised session, or just

in any way revoke a token that shouldn't be in use, you can't. Unless you carry around some sort of tracking system to track all of these issued tokens, but then maybe keep a list of token IDs, but if you're doing that, you may as well use that for your sessions anyway. Another way around this that people do is give their sessions a tokens a super short expiry, but that doesn't really benefit you because if the expiry is too short, you still have to round trip to the database, and having an expiry is not the same as actually being able to properly revoke a token. Problem number two, which didn't quite land top spot. JSON as a format is truly

awful. Yes, it's somewhat more aesthetic and accessible than XML, but I am prepared to die on this hill. It's also a very weakly defined format. If you encode JSON in one library, there are no guarantees whatsoever that it will decode the same in another. Hell, if you encode junk data in one library and then try and decode it in the same library, there are no guarantees you'll even get the same data back. So, you better hope you're validating all the user data that you're allowing them to stuff into these tokens before, you know, you use it in your login authentication infrastructure. So, this causes problems with things like field ordering. If you have duplicate keys, what order they turn up

in in the other libraries? Knowing the different data types of fields, so should numbers be integers, strings, floats? What about complex numbers? They have letters in them. Who knows? Good luck figuring it out and making sure that every one of your libraries that you're trying to interop with do it the same way. Problem number three, I can summarize as YAGNI, you are not going to need it. You don't need JWTs, they almost always unnecessarily complex, and it increases your attack surface attack surface, as mentioned, on critical infrastructure infrastructure such as login and authentication. You're not Facebook, and you're not Google, and you're not trying to authenticate millions of users a second, so don't bother trying to be

stateless. You don't need to do it. Uh if you really need to, just use Redis or any memory database, but most times databases are going to be pretty fine. Problem four. JWTs are way, way too flexible. The official spec has about 13 types of um signature algorithms, which has been the source of tons of critical impact bugs. Better all that's just for the JSON Web Signatures part. The JSON Web Encryption part has another 17 different signature algorithms uh encryption algorithms, I'm sorry. Uh so, that's a choice of 30 algorithms if you have any arbitrary JWT library that it might having might be having to support. If you switch the algorithm type in one of the JWT headers from an asymmetric

type like HS256 to a symmetric algorithm type like RSA256, you can actually confuse the server into signing valid tokens. And you can send it arbitrary data, you switch around the header type, and it will then perform the wrong algorithm on your data and sign you valid data. This alone has been the source of tons of critical bugs. It is now known as algorithm confusion and is its own bug category. There is a widely accepted axiom in cryptography that is the fewer options, the better. For ex- good example, you can see WireGuard, which is like a modern VPN protocol. In WireGuard, it mandates that every different component of the system has a very specific algorithm use. So, key exchange

uses a specific algorithm, data encryption uses a specific algorithm, and so on. Under the thinking that any number of options is bound to have some bad ones, so you may as well fix everyone onto a one version, and then when that version turns out to have problems in it, you can just upgrade the protocol version and move everyone to a different set. JWTs, on the other hand, have the option of using none as an algorithm, and yeah, let's just let's just go to the next slide. Problem five is foot guns, which is a wonderful name given to something that instead of guiding the user to use it safely, instead helps them to shoot themselves in the foot. A foot gun. So,

I mentioned the none algorithm, and some of you might be getting flashbacks to TLS null ciphers, which uh as you may have guessed, this particular algorithm doesn't bother signing the data at all. And yeah, a ton of libraries have been vulnerable to this really stupid and hilarious bug chain. So, you take your JWT, you change your user ID to admin, you change your algorithm type to none, and yeah, that's it. You have admin now. Great. And that's been a real bug in lots of like, you know, multi-billion dollar like companies I'm not going to name because I've been told not to. There's also the key ID parameter, which has led to injection attacks because oh yeah,

the token can tell you how it wants you to verify it, this user-supplied token. Yeah, basically, they're way too configurable and too easy to configure wrongly. And yeah, uh having asymmetric and symmetric algorithms is also painful. I touched on this in the last slide, but it really deserves its own one. In order to find out if a user is holding a valid authentication token, you need to take this random blob of base64-encoded data, decode that, then parse that as JSON. You then have to figure out which algorithm type is specified out of potential 30, um where the key server is located because yes, the JWT can tell you where the keys to decode that token should be

located. Uh what key ID to use, and then seriously, read the spec. It's a Kafkaesque nightmare. And you have to do all the above on user-supplied data in your authorization and authentication logic. Can I just extol the virtues of random strings as cookies just one more time? So, you may be thinking, I really need to scale to billions of requests a second. I can't possibly use a database to authenticate my users. Ah, and uh no, you don't. You'll be fine with a database or Redis. Just use it. Another argument I hear for use of JWTs is that they're easily interoperable with different languages and libraries, which I answer so are protocol buffers. Proto buffs are really good and they

don't have anywhere near the same amount of problems that JWTs do inherently. The third argument is that everyone else is doing it. It can't be that bad. To which I will tell you, dear developer, to be the change you want to see in the world and just because other people are doing it doesn't mean it fits your use case and it doesn't mean that you should. I bet you use Kubernetes to host a static blog. And the final worst argument storing JWTs in local storage, which is a quite a lot storing JWTs in local storage, which is quite a common method of actually doing it, protects you from cross-site request forgery. So, I'm now more secure. Stop

being lazy, implement your CSRF tokens, and JWTs are not a security control. If you do use it that way, that is a side effect, but you've now opened yourself back up to cross-site cross-site scripting attacks again because you don't have the protections of HTTP only or anything else. I'm far away. I'm putting the slide deck up online, so Anyway. Anyway, come to the solution. Just use cookies, random strings, UUIDs, whatever you want. Send your client a cookie with a random ID, store that random ID in a database, and that's your that's your session infrastructure. Don't bother with JWTs. So, you can either check it whenever a user sends a request or set up some middleware. It's not that

difficult. Almost every library has a method to do this these days. And cookies, as mentioned, have the have the added benefit of having been around for a very long time. Because of this, they're a very well-understood attack surface and they have some pretty good browser protections. So, as mentioned, there's HTTP only, which means you won't send cookies over unsecure connections. There's secure flags, which is the one I just mentioned, and HTTP only means you won't send it over the other connections. One of those, I forget which one because obviously I'm presenting this live, means you can't access the cookies from JavaScript, which protects you from cross-site scripting attacks, but JWTs don't have these if you're storing them in local

storage. Anyway. Point of the point is you're throwing away a bunch of very good protections that have been built up around cookies by using this new stack. And there's not really that much that you're getting out of this new technology that you couldn't do other ways that isn't introducing you to loads of other bugs. To summarize this talk in one slide, I have this. Stop using JWTs for session tokens. Just use cookies with random identifiers and your life will be much simpler and you will be much happier.

As I mentioned, I based this talk on a bunch of amazing material I read from people much smarter than me, and I tried to capture all the most relevant references here, but I'll try and put up the slide deck somewhere online. I don't know how that's going to work, but yeah. If that doesn't work, feel free to @ me on Twitter. And yeah, questions.

Yeah.

Maybe I'll mouse I'll mouse over them for you.

Mandates JWTs. Yes, it does.

When you are required to use JWTs such as in that case, the best thing you can do to try and work around the inherent problems is specifically mandate every single configurable option that you're looking for. So, instead of accepting random algorithm types from the client, you will expect a particular algorithm. You will ignore all Yeah, you're basically you're basically taking the standard and constraining it down to a very specific set of options.

Nothing I'm going to say on stage.

Question? Any questions? All right. If you think of any later, then feel free to @ me on Twitter, and thank you very much.