JWT Implementation Pitfalls, Security Threats, and Our Approach to Mitigate Them
Published: 2023-09-23 14:47:41

## JSON Web Tokens

There are many excellent introductions to JWTs, so for the purposes of this discussion we will focus on the structure.

JWTs are typically transmitted as base-64 encoded strings, and are composed of three parts separated by periods:

1. A header containing metadata about the token itself
2. The payload, a JSON-formatted set of claims
3. A signature that can be used to verify the contents of the payload

For example, this JWT

```json
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBTbGFzaElEIiwiaWF0IjoxNTE2MjM5MDIyfQ.4cL42NsNCXLPEvmvNGxHN3wLuarpp98wwezHnSt2fqg
```

comprises the following parts

| Part      | Encoded value                                                                  | Decoded value                                                    | Description                                                                                       |
| --------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| Header    | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9                                           | { "alg": "HS256", "typ": "JWT"}                                  | Indicates that this is a JWT and that it was hashed with the HS256 algorithm (HMAC using SHA-256) |
| Payload   | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBTbGFzaElEIiwiaWF0IjoxNTE2MjM5MDIyfQ | {"sub": "1234567890", "name": "SlashID User", "iat": 1516239022} | Payload with claims about a user and the token                                                    |
| Signature | 4cL42NsNCXLPEvmvNGxHN3wLuarpp98wwezHnSt2fqg                                    | N/A                                                              | The signature generated using the HS256 algorithm that verifies the payload                       |

The important aspect of this is that the JWT is **signed, meaning that the claims in the payload can be verified**, if one has access to the appropriate cryptographic key.

The algorithm used for the signature is stored in the Header (`alg`) and as we'll see later in the article, this becomes the source of a lot of issues with JWTs.

The signature of a JWT token is calculated as follows:

```jsx
signAndhash(base64UrlEncode(header) + '.' + base64UrlEncode(payload))
```

Where `signAndhash` is the signing and hashing algorithms specified in the `alg` header field. The [JOSE IANA](https://www.iana.org/assignments/jose/jose.xhtml#web-signature-encryption-algorithms) page contains the list of supported algorithms.

In the example above HS256 stands for HMAC using SHA-256 and the `secret` is a 256-bit key.

> Signing is _not_ the same as encryption - even without the cryptographic key for verifying, anybody can decode the token payload and inspect the contents.

## Naming convention

There are many standards associated with JWTs, it is useful to clarify a few different formats as we'll use them throughout the article.

**JWT (JSON Web Token): JSON-based claims format using JOSE for protection**

JOSE (Javascript Object Signing and Encryption): set of open standards, including:

- **JWS (JSON Web Signature)**: JOSE standard for cryptographic authentication
- **JWE (JSON Web Encryption)**: JOSE standard for encryption
- **JWA (JSON Web Algorithms)**: cryptographic algorithms for use in JWS/JWE
- **JWK (JSON Web Keys)**: JSON-based format to represent JOSE keys

The JWT specification is relatively limited as it only defines the format for representing information ("claims") as a JSON object that can be transferred between two parties.
In practice, the JWT spec is extended by both the JSON Web Signature (JWS) and JSON Web Encryption (JWE) specifications, which define concrete ways of actually implementing JWTs.

## Anatomy of a JWT-related bug

A key aspect of JWTs, and one of the reasons why they have become so popular, is that they can be used in a stateless manner. In other words, the server doesn't store a copy of the JWTs it mints. As a result, to check the validity of the token both the client and the server need to verify their signature. The validity of the signature is how we prove the integrity of the token.

Generally vulnerabilities in JWT implementations rely on either a failure to validate a token signature, a signature bypass, or a weak/insecure secret used to encrypt or sign a token.

Fundamentally three design choices make JWT implementations prone to issues:

1. Deciding the decryption/validation algorithm based on untrusted ciphertext
2. Allowing broken algorithms (RSA PKCS#1 v1.5 encryption) and "none"
3. Allowing for very complex signing options. For instance, supporting X.509 Certificate Chain.

Let's see some of the most common issues with JWTs.

### The "none" Algorithm

The `none` algorithm is intended to be used for situations where the integrity of the token has already been verified. Unfortunately, some libraries treat tokens signed with the `none` algorithm as a valid token with a verified signature. This would allow an attacker to bypass signature checks and mint valid JWT tokens.

> Solution: Always sanitize the `alg` field and reject tokens signed with the `none` algorithm.

### "Billion hashes attack"

Tervoort recently disclosed at Black Hat a new attack [pattern](https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf).

JWT tokens support various families of `PBES2` as signing/encryption algorithms. The `p2c` header parameter is required when using `PBES2` and it is used to specify the PBKDF2 iteration [count](https://en.wikipedia.org/wiki/PBKDF2).

An unauthenticated attacker could use the parameter to DoS a server by specifing a very large `p2c` value resulting in billions of hashing function iterations per verification attempt.

> Solution: Always sanitize the `p2c` parameter.

### Brute-forcing or stealing secret keys

Some signing algorithms, such as HS256 (HMAC + SHA-256), use an arbitrary string as the secret key. It's crucial that this secret can't be easily guessed, brute-forced by an attacker or stolen.

An attacker with the secret key would be able to create JWTs with any header and payload values they like, then use the key to re-sign the token with a valid signature.

> Solution: Avoid weak secret keys, implement frequent key rotation.

### Algorithm confusion

As discussed, JWTs support a variety of different algorithms (including some broken ones) with significantly different verification processes. Many libraries provide a single, algorithm-agnostic method for verifying signatures. These methods rely on the `alg` parameter in the token's header to determine the type of verification they should perform.

Problems arise when developers use a generic signature method and assume that it will exclusively handle JWTs signed using an asymmetric algorithm like `RS256`. Due to this flawed assumption, they may end up in a "type confusion" type of scenario. Specifically, a scenario where the public key of a keypair is used as an HMAC secret for a symmetric cypher instead.

An attacker in this case can send a token signed using a symmetric algorithm like `HS256` instead of an asymmetric one. This means that an attacker could sign the token using HS256 and the static public key used by the server to verify signatures, and the server will use the same public key to verify the **symmetric** signature thus completely bypassing the signature verification process.

> Solution: Always verify the alg parameter and ensure that the key passed to the verification function matches the type of algorithm used for the signature.

### Key injection/self-signed JWT

Although only the `alg` parameter is mandatory for a token, JWT headers often contain several other parameters. Some of the more common ones are:

- **jwk** (JSON Web Key) - Provides an embedded JSON object representing the key.

- **jku** (JSON Web Key Set URL) - Provides a URL from which servers can fetch a set of keys to verify signatures.

- **kid** (Key ID) - Provides an ID that servers can use to identify the correct key in cases where there are multiple keys to choose from.

These user-controllable parameters each tell the recipient server which key to use when verifying the signature.

#### Injecting self-signed JWTs via the jwk parameter

The `jwk` header parameter allows an attacker to specify an arbitrary key to verify the signature of a token. Servers should only use a limited allow-list of public keys to verify JWT signatures. However, default implementations of JWT verification libraries allow for arbitrary signatures to be used hence the developer has to allow-list specific keys or otherwise an attacker could bypass the signature verification process.

> Solution: Disallow the usage of `jwk` or have an allow-list of valid keys.

#### Injecting self-signed JWTs via the jku parameter

Similar to the example below it is crucial that the keys passed to the verification function via the `jku` parameters are part of an allow-list. Further the implementer should also
have an allow-list of domains and valid TLS certificates for those domains.

In fact, JWK Sets like this are often exposed publicly via a standard endpoint, such as `/.well-known/jwks.json` - if a domain is subject to a watering-hole attack or the verification function doesn't verify the domain an attacker is able to bypass signature verification.

> Solution: Disallow the usage of `jku`` or have an allow-list of valid keys, trusted domains, and valid TLS certificates for those domains.

#### Injecting self-signed JWTs via the kid parameter

Verification keys are often stored as a JWK Set. In this case, the server may simply look for the JWK with the same `kid` as the token. However, the `kid` is an arbitrary string and it's up to the developer to decide how to use it to find the correct key in the JWK Set.

The `kid` parameter could be used for a command injection attack without proper sanitization. For example, an attacker might be able to use it to force a directory traversal attack pointing the verification function to a static, well-known file like `/dev/null` which would result in an empty string used for verification and often result in a signature bypass.

Another example is if the server stores its verification keys in a database, the `kid` parameter is also a potential vector for SQL injection attacks.

> Solution: Always sanitize the `kid` parameter.

## The SlashID approach

The issues above are just some of the common ones found in libraries, but many others keep coming up due to the design flaws described above. At SlashID, we strive to help customers secure their identities so we took a principled approach to the problem by abstracting it away for developers.

In a previous blogpost we've discussed some of the implementation details of our we mint and verify JWT [tokens](https://www.slashid.dev/blog/tink-jwt-ecdsa/).

But we've gone further and added a JWT verification plugin to [Gate](https://www.gateproject.io/).

The verification plugin works both with tokens issued by SlashID as well as tokens issued by a [third-party](https://developer.slashid.dev/docs/gate/plugins/validate-jwt).

To address the issues described above, we employ several countermeasures:

1. Only support pre-defined signing algorithms
2. Rotate signing keys frequently and adopt a vaulting solution to store the private key
3. Verify and pin TLS certificates for JWKS
4. Maintain an allow-list of valid domains
5. Disallow unsafe header parameters

By deploying Gate with the JWT verification plugin, developers can offload the complexity and risk of verifying JWT tokens to SlashID.

Let's see an example in [action](https://www.slashid.dev/blog/jwt-risks/#gate-configuration).


## Conclusion

In this brief post we've shown how JWT tokens while ubiquitous and simple-looking at first, are fraught with risk. Gate is an effective and easy way to offload that
effort from application developers.

We’d love to hear any feedback you may have! Try out Gate with a [free account](https://console.slashid.dev/signup/gate).