I've encountered issues like this in various systems using JWT at this point. The real problem is that developers blacklist the algorithms they don't want. Instead, the verification code should explicitly whitelist which algorithms you support.
More specifically, you can't even rely on using the 'alg' parameter before successful signature verification with any level of authority: after all, it is protected by the signature it declares the algorithm for itself. So even with a whitelist, there is the potential of downgrade attacks.
In other words, don't even use a whitelist, use a single specific expected algorithm.
I have a coworker who does shit like this all the time.
The list of supported options is not only knowable, but changes very slowly. Which means it's almost certainly known at commit time. Just enumerate them. By hand. Oh no, you might have to type in some text that exists somewhere else! Quelle horreur!
The list of unsupported options is unknowable. The list of string or path interpolation bugs is knowable, but isn't known by the sort of person who thinks a whitelist is a bad idea. Build a lookup table and stop trying to be clever.
fn verify_jwt(
// The input from the network/user; the JWT we will be verifying
untrusted_jwt: String,
// *The* way in which we expect JWTs to be issued / we will be verifying against:
verifier: Verifier,
) -> ... {
// Basic sanity checks & decoding.
let untrusted_jwt: Jwt = ...;
if !verifier.acceptable_alg(untrusted_jwt.alg) {
bail;
}
verifier.verify(untrusted_jwt)
}
Where `verifier` is something like `JwtRsa` or `JwtEd25519`, or `JwtHmac`. The verifier should know what few, limited algorithms to look for, and it should reject anything and everything that's not under its domain area. There's no blacklist, no guesswork. (I don't even think I'd have `acceptable_alg`; just let `verify` do that work; it has to look at that field anyways to set up, e.g., the hasher.)
I'm largely omitting¹ JWE, so perhaps there's some hidden dragon in there, or perhaps we just handle those completely separately. But for JWS, am I missing something? Unless you pass `NoneVerifier`, in which case you're explicitly opting in to alg: none and it's goriness.
Sadly, I don't think any of the top three Rust libraries do this.
¹I'm also omitting that RSA has many attached hash algorithms in JWS; one can imagine that JwtRsa might let you configure what hashers it will/will not use. As it is, some libraries take sort of this form, but split the key material off from the algorithm … letting you pass craziness like an RSA key material and HMAC-SHA256, which makes no sense. That is, what hash algorithms (if any) are possible is a function of the type of key material coming in.
¹I'm also omitting verification of the claims, which I think a library should generally handle.
Is there some good reason to prefer a blacklist in this case, that I'm not thinking of, that might change my reaction to this from "uh, maybe I need to entirely re-think my assumption that Auth0 is any better at this whole securing-users thing than I am"? My immediate and ongoing reaction to the headline was and is, "wait, a blacklist? WTF!"
The purpose of a blacklist is to seamlessly support newer algorithms invented later.
For example consider SHA1 replaced by SHA256 by SHA512. If some components is hardcoded to only accept SHA1, it can't use the newer algorithms and potentially block adoption by other components.
So it's somewhat normal design for future proofing, but it's a bit stupid for security/authentication stuff which sole purpose is to verify messages.
I get why you'd use a blacklist in general, but it seems obvious to me that in a case like this you do not want to allow any algo you haven't cleared first. Especially since it may not be better! What if your underlying library decides it's gonna support md5 hashes for signatures?
What system of application deployment and maintenance are you picturing where a new crypto algorithm shows up on a machine without an opportunity to make a one-line commit to your codebase?
And in a SaaS setting, what operational environment is going to want you to start using a new cryptographic algorithm with no formalism around the process? Someone is going to want to flip a proverbial switch.
These libraries doing the crypto, often managed by the OS, are upgrading the TLS protocol and ciphers on the fly with most applications having no awareness of what's going on.
I would argue the opposite. openSSL is such a can of worms at this point that if you suggested this sort of magic thinking in a design meeting, you'd have a fight on your hands and I wouldn't be the only person arguing with you.
> Instead, the verification code should explicitly whitelist which algorithms you support.
What libraries are you using? I just looked through the auth code for a project I'm working on (which uses `jsonwebtoken`) and it has an option to whitelist algorithms in the `jwt.verify` method.
Various, been a while since I wrote code using them myself.
Often JWT tokens come from sources other than our own and they will have passed through user agent or client land. Don't trust anything in them unless you verified them.
edit: good on that library! That's what it should do. Clearly auth0's code did not do that though, it should never have accepted any variant of 'none' in the first place.
Hah. They could still improve it by only accepting a single algorithm, rather than a list.
edit: though there could be some internal use cases where you want a list, but it's a tradeoff between flexibility and making it easy for people to shoot themselves in the foot.
More specifically, you can't even rely on using the 'alg' parameter before successful signature verification with any level of authority: after all, it is protected by the signature it declares the algorithm for itself. So even with a whitelist, there is the potential of downgrade attacks.
In other words, don't even use a whitelist, use a single specific expected algorithm.