DEV Community

vincenzoiozzo
vincenzoiozzo

Posted on

JWT Implementation Pitfalls, Security Threats, and Our Approach to Mitigate Them

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvZSBTbGFzaElEIiwiaWF0IjoxNTE2MjM5MDIyfQ.4cL42NsNCXLPEvmvNGxHN3wLuarpp98wwezHnSt2fqg
Enter fullscreen mode Exit fullscreen mode

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:

signAndhash(base64UrlEncode(header) + '.' + base64UrlEncode(payload))
Enter fullscreen mode Exit fullscreen mode

Where signAndhash is the signing and hashing algorithms specified in the alg header field. The JOSE IANA 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.

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.

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.

But we've gone further and added a JWT verification plugin to Gate.

The verification plugin works both with tokens issued by SlashID as well as tokens issued by a third-party.

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.

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.

Top comments (0)