{"id":10620,"date":"2021-12-22T17:07:32","date_gmt":"2021-12-22T17:07:32","guid":{"rendered":"https:\/\/staging-site.42crunch.com\/?p=10620"},"modified":"2022-11-24T11:26:01","modified_gmt":"2022-11-24T11:26:01","slug":"7-ways-to-avoid-jwt-pitfalls","status":"publish","type":"post","link":"https:\/\/staging2022.42crunch.com\/7-ways-to-avoid-jwt-pitfalls\/","title":{"rendered":"7 Ways to Avoid JWT Security Pitfalls"},"content":{"rendered":"

Dec 22nd 2021. \u00a0<\/span>Author: Dr. Philippe de Ryck, Pragmatic Web Security,<\/span><\/h4>\n

Like them or hate them, JSON Web Tokens (JWT) are everywhere.<\/span><\/p>\n

OAuth 2.0 and OpenID Connect rely heavily on JWTs. Many applications use JWTs to implement custom security mechanisms. And every language or framework offers plenty of support for JWTs.<\/span><\/p>\n

Unfortunately, JWTs also lie at the heart of numerous API security failures. Handling JWTs securely is often challenging and error-prone. And because JWTs are typically used for sensitive features, such as authentication and authorization, the impact of such failures can be quite critical.<\/span><\/p>\n

Plenty of articles (<\/i><\/b>https:\/\/pragmaticwebsecurity.com\/articles\/apisecurity\/hard-parts-of-jwt.html<\/i><\/b><\/a>) discuss the technicalities of JWTs, but not too many focus on concrete developer guidelines to avoid security pitfalls.<\/i><\/b> In this article, we cover seven techniques that will help you avoid JWT security pitfalls.<\/span><\/p>\n

 <\/p>\n

1. Use a Static Algorithm Configuration<\/span><\/h2>\n

In 2015, the JWT world was shaken up by the disclosure of a JWT type confusion attack (<\/span>https:\/\/auth0.com\/blog\/critical-vulnerabilities-in-json-web-token-libraries\/<\/span><\/a>). In such an attack, the application expects a token signed by a private key, but can be confused to accept a token protected by an HMAC. As a result, an attacker can craft arbitrary tokens that are accepted as valid tokens, typically resulting in an authorization bypass.<\/span><\/p>\n

In hindsight, this problem is quite silly to have, but hard to eradicate. Even to this day, applications still suffer from similar vulnerabilities. The problem is that many applications use the <\/span>alg<\/span> claim before the token is verified, allowing the attacker to tamper with that value.<\/span><\/p>\n

So what’s the best way to avoid such issues?<\/span><\/p>\n

Do not trust the value of the <\/i><\/b>alg<\/i><\/b> header claim.<\/i><\/b> That’s it.<\/span><\/p>\n

 <\/p>\n

In most cases, the application knows the exact signature algorithm it expects. As a result, the algorithm can be hardcoded, as shown in the example below.<\/span><\/p>\n

\/\/ Example using the ‘jsonwebtoken’ library for Node<\/span><\/i><\/p>\n

const<\/span> signedToken = jwt.sign(data, signingKey, { <\/span>algorithm<\/span>: <\/span>“PS256”<\/span> });<\/span><\/p>\n

When the application cannot hardcode the algorithm, it should maintain a list of acceptable values (e.g., <\/span>[“RS256”, “PS256”]<\/span>. The application can then verify the value of the <\/span>alg<\/span> claim against this list.<\/span><\/p>\n

Note that the list should either contain HMAC algorithms or asymmetric algorithms, but avoid mixing them together. This report (<\/span>https:\/\/github.com\/firebase\/php-jwt\/issues\/351<\/span><\/a>) for the Firebase <\/span>php-jwt<\/span> discusses how handling both types in the same code path can still result in confusion. So in the rare case that you need to handle both HMACs and signatures simultaneously, make sure you split that functionality into separate code paths.<\/span><\/p>\n

Key takeaway: a token verification procedure should only accept a single type of token.<\/i><\/b><\/p>\n

 <\/p>\n

2. Use Explicit Typing<\/span><\/h2>\n

Take a look at the decoded JWT header and payload in the snippet below. Can you tell what this JWT is used for?<\/span><\/p>\n

<\/code><\/pre>\n
{\n\n\u00a0\u00a0"alg": "PS256",\n\u00a0\u00a0"typ": "JWT",\n\u00a0\u00a0"kid": "NTVBOTNjEyMw"\n\n}\n{\n\u00a0\u00a0"iss": "https:\/\/sts.example.com\/",\n\u00a0\u00a0"sub": "auth0|60adf0e02cda61006e4a9110",\n\u00a0\u00a0"aud": "https:\/\/api.example.com",\n\u00a0\u00a0"iat": 1638283665,\n\u00a0\u00a0"exp": 1638370065,\n\u00a0\u00a0"azp": "DtsTliLAWq3JXIwaoPQzl8vXhNI6qGnb",\n\u00a0\u00a0"scope": "openid email offline_access"\n}<\/code><\/pre>\n
<\/code><\/pre>\n

The JWT shown here is an OAuth 2.0 access token. You may have guessed this correctly because you’re pretty familiar with access tokens. Otherwise, there’s no way to determine the purpose of a JWT. And precisely that lack of information can lead to a problem known as token confusion.<\/span><\/p>\n

What is token confusion all about?<\/i><\/b><\/p>\n

Let’s explore with another example. What’s the purpose of the JWT shown below?<\/span><\/p>\n

{\n\u00a0\u00a0"alg": "PS256",\n\u00a0\u00a0"typ": "JWT",\n\u00a0\u00a0"kid": "NTVBOTNjEyMw"\n}\n\n{\n\u00a0\u00a0"email": "philippe@pragmaticwebsecurity.com",\n\u00a0\u00a0"email_verified": false,\n\u00a0\u00a0"iss": "https:\/\/sts.example.com\/",\n\u00a0\u00a0"sub": "auth0|60adf0e02cda61006e4a9110",\n\u00a0\u00a0"aud": "DtsTliLAWq3JXIwaoPQzl8vXhNI6qGnb",\n\u00a0\u00a0"iat": 1638283665,\n\u00a0\u00a0"exp": 1638319665\n}<\/code><\/pre>\n

This time, the token is an OpenID Connect identity token. Client applications typically request an identity token and access token from an OAuth 2.0 authorization server. The identity token contains information about the authenticated user. The access token is used to access APIs on behalf of the authenticated user.<\/span><\/p>\n

Depending on the authorization server’s configuration, these tokens can look eerily similar. As a result, an API may be tricked into accepting an identity token as an access token, which can result in authorization bypass attacks.<\/span><\/p>\n

To avoid this problem, JWT tokens should be explicitly typed. The snippet below shows our access token from before, but with an explicit purpose in the <\/span>typ<\/span> header claim.<\/span><\/p>\n

{\n\u00a0\u00a0"alg": "PS256",\n\u00a0\u00a0"typ": "at+jwt",\n\u00a0\u00a0"kid": "NTVBOTNjEyMw"\n}\n\n{\n\u00a0\u00a0"iss": "https:\/\/sts.example.com\/",\n\u00a0\u00a0"sub": "auth0|60adf0e02cda61006e4a9110",\n\u00a0\u00a0"aud": "https:\/\/api.example.com",\n\u00a0\u00a0"iat": 1638283665,\n\u00a0\u00a0"exp": 1638370065,\n\u00a0\u00a0"azp": "DtsTliLAWq3JXIwaoPQzl8vXhNI6qGnb",\n\u00a0\u00a0"scope": "openid email offline_access"\n}\n<\/code><\/pre>\n

Explicitly including the purpose in the claims removes any ambiguity about the meaning of the token. APIs can be configured to only accept tokens with the at+jwt type, preventing an attacker from using any other token type.<\/p>\n

Note that the value of the <\/span>typ<\/span> header is an arbitrary string. Consequently, you can use any type you prefer for custom-generated JWTs. The token below shows the header of an email verification token with a custom type.<\/span><\/p>\n

{\n\u00a0\u00a0"alg": "PS256",\n\u00a0\u00a0"typ": "email-verification+jwt",\n}<\/code><\/pre>\n

Key takeaway: when generating JWTs, include an explicit type in the <\/i><\/b>typ<\/i><\/b> header claim.<\/i><\/b><\/p>\n

 <\/p>\n

3. Go All Out on Metadata<\/span><\/h2>\n

The JWT specification lists several reserved claims with a specific meaning. Some of these claims are crucial to determine the validity of a JWT. For example, a JWT token can have an expiration date (<\/span>exp<\/span>), a not-before date (<\/span>nbf<\/span>), and a time of issuing (<\/span>iat<\/span>). Most developers are well-aware of these claims, and most libraries automatically verify them when checking the signature of a JWT.<\/span><\/p>\n

Apart from these time-based claims, there are two more reserved claims with a critical role for security: the issuer (<\/span>iss<\/span>) and the audience (<\/span>aud<\/span>).<\/span><\/p>\n

The <\/span>iss<\/span> claim indicates the identity of the issuer of a JWT. The value is an arbitrary string, but URL-based identifiers are commonly used as the value. For example, in an OAuth 2.0 access token, the value of the issuer is the URL of the authorization server.<\/span><\/p>\n

The <\/span>aud<\/span> claim indicates the identity of the party supposed to consume the JWT. As the issuer, the value is an arbitrary string, but URL-based identifiers are pretty standard. In our access token example, the <\/span>aud<\/span> claim indicates that this token is supposed to be consumed by our backend API.<\/span><\/p>\n

Unfortunately, having these claims in the token is useless if nobody checks them. In our access token example, the API should ensure that the values of the <\/span>iss<\/span> and <\/span>aud<\/span> claims are valid. A failure to do so can result in authorization failures.<\/span><\/p>\n

But how does a scenario like that happen?<\/i><\/b><\/p>\n

 <\/p>\n

Cases like this are pretty common in complex architectures, where multiple APIs accept tokens coming from the same issuer. A good example is an API ecosystem accepting access tokens from the same authorization server, as illustrated below.<\/span><\/p>\n

\"\"<\/p>\n

The snippet below shows a token that represents the authority to access API 1, but not API 2, as indicated by the <\/span>aud<\/span> claim.<\/span><\/p>\n

{\n\u00a0\u00a0"iss": "https:\/\/sts.example.com\/",\n\u00a0\u00a0"aud": "https:\/\/api1.example.com",\n\u00a0\u00a0...\n}<\/code><\/pre>\n

Both APIs expect tokens from the authorization server, so the signature verification step will succeed. However, when these APIs fail to verify the audience claim, a malicious user or client can trick API 2 into performing operations with a token intended for API 1.<\/span><\/p>\n

Avoiding this issue is not that complicated. Most JWT libraries support the verification of the issuer and the audience out of the box. All it takes to enable this behavior is providing the library with the expected value, as shown in the snippet below.<\/span><\/p>\n

\/\/ Example using the 'java-jwt' library for Java \nAlgorithm algorithm = Algorithm.RSA256(publicKey, null);\n JWTVerifier verifier = JWT.require(algorithm)\n \u00a0\u00a0\u00a0\u00a0.withIssuer("https:\/\/sts.example.com\/")\n \u00a0\u00a0\u00a0\u00a0.withAudience("https:\/\/api1.example.com\/")\n \u00a0\u00a0\u00a0\u00a0.build(); DecodedJWT jwt = verifier.verify(token);<\/code><\/pre>\n

 <\/p>\n

Key takeaway: always include and verify the target audience of a JWT.<\/i><\/b><\/p>\n

 <\/p>\n

4. Treat your Secrets as Secrets<\/span><\/h2>\n

To sign a JWT, the application requires access to a secret. Similarly, verifying an HMAC on a JWT requires access to the HMAC secret. Intuitively, we all know that secrets should be kept secret. Unfortunately, the current state of practice tells a different story.<\/span><\/p>\n

As illustrated by various studies, it turns out that secrets are often hardcoded in applications (<\/span>https:\/\/r2c.dev\/blog\/2020\/hardcoded-secrets-unverified-tokens-and-other-common-jwt-mistakes\/<\/span><\/a>). As a result, the secret is easily exposed, which can lead to the total compromise of an application.<\/span><\/p>\n

Even worse, some applications simply re-use a hard coded secret from examples found in documentation or Stack Overflow. By observing a single JWT, an attacker can quickly recover such a publicly-known secret using tools such as Burp’s JWT Heartbreaker extension (<\/span>https:\/\/portswigger.net\/daily-swig\/jwt-heartbreaker-offers-remedy-for-weak-json-web-tokens<\/span><\/a>).<\/span><\/p>\n

This problem can be avoided altogether by treating secrets as secrets. First, secrets should be freshly generated and not be shared across dev and prod environments.<\/span><\/p>\n

Second, secrets should be provided to the application without hardcoding them. A simple approach is to provide secrets as environment variables. Even better is to rely on the secret management service of your platform (e.g., AWS Secrets Manager, Azure Key Vault) or an external service, such as Hashicorp’s Vault (<\/span>https:\/\/www.vaultproject.io\/<\/span><\/a>).<\/span><\/p>\n

Key takeaway: do not include HMAC secrets or private keys in code.<\/i><\/b><\/p>\n

 <\/p>\n

5. Bring Down the Testing Hammer<\/span><\/h2>\n

Even with the best intentions, it is still easy to make mistakes when handling JWTs. This vulnerability in Apache Pulsar is the perfect example (<\/span>https:\/\/portswigger.net\/daily-swig\/apache-pulsar-bug-allowed-account-takeovers-in-certain-configurations<\/span><\/a>). In this vulnerability, the application accepted tokens with the <\/span>alg<\/span> claim set to <\/span>none<\/span>. This effectively means that the token is unsigned, allowing an attacker to craft valid tokens with arbitrary claims. In this particular case, the vulnerability resulted in an authorization bypass.<\/span><\/p>\n

So, what happened here?<\/i><\/b><\/p>\n

\"\"<\/p>\n

As you can see, the mistake occurred in a single line of code. The application used the <\/span>parse<\/span> function of the JJWT library, instead of the <\/span>parseClaimsJws<\/span> function.<\/span><\/p>\n

Of course! What a silly mistake.<\/span><\/p>\n

In reality, things are not that simple.<\/i><\/b><\/p>\n

The documentation of the <\/span>parse<\/span> function (<\/span>https:\/\/github.com\/jwtk\/jjwt\/blob\/master\/api\/src\/main\/java\/io\/jsonwebtoken\/JwtParser.java<\/span><\/a> ) does not explicitly mention this insecure behavior. Additionally, the library has five other functions that can be used to process a JWT. In a nutshell, it is virtually impossible for a developer to figure this out while writing code.<\/span><\/p>\n

That’s why we need testing!<\/span><\/p>\n

Every API endpoint that accepts JWTs should be rigorously tested to ensure it handles JWTs securely. And by <\/span>rigorously<\/span><\/i>, I mean bringing down the hammer!<\/span><\/p>\n

Here’s a list of invalid JWTs that a test suite should throw at any JWT verification function:<\/span><\/p>\n