Context
JSON Web Tokens (JWTs) are a standardized means of transmitting cryptographically-signed JSON data between systems, typically used for conveying "claims" or user-related information as part of authentication, session management, and access control mechanisms. While they can contain any type of data, they are most often used in highly distributed websites where users need to interact with multiple backend servers, since all the necessary information for the server is stored within the JWT itself, unlike classic session tokens.
When a server issues a token, it generates a signature by hashing the token's header and payload using a secret signature key. In some cases, the resulting hash is also encrypted. This mechanism ensures that the token's data has not been tampered with since it was issued, since any modification to the header or payload would result in a signature mismatch. The signature is directly derived from the rest of the token, so even a single byte change in the header or payload would cause the signature to no longer match.
JWT attacks typically involve an attacker sending modified tokens to the server to bypass authentication and access controls, and impersonate another authenticated user. These attacks can potentially compromise the entire website and its users, since JWTs are commonly used for authentication, session management, and access control.
The vulnerabilities in JWTs usually stem from faulty token management within the application, rather than flaws in the JWT specification itself. JWT specifications are relatively flexible by design, which allows web developers to make implementation decisions themselves. However, this flexibility can inadvertently introduce vulnerabilities, even when using libraries. For example, a common vulnerability is failing to correctly verify the token's signature, which allows an attacker to falsify values in the token's payload. Additionally, the reliability of the signature heavily depends on the secret key remaining secret. If the key is leaked or can be guessed, an attacker can generate a valid signature for any arbitrary token, compromising the entire mechanism.
Impact
JWT attacks can have severe consequences since an attacker who is able to create valid tokens with arbitrary values can potentially gain unauthorized access to the system. By doing so, they may be able to elevate their privileges or impersonate other users, thereby gaining full control over their accounts.
Exploitation of the flaw
Servers are designed to not store any information about the JWTs they issue, and each token is a self-contained entity. While this design has several advantages, it also presents a fundamental problem - the server has no knowledge of the original contents of the token or its original signature. Consequently, if the server fails to correctly verify the signature, an attacker can make arbitrary modifications to the rest of the token without any hindrance.
{
"username": "carlos",
"isAdmin": false
}
In the event that the server identifies the session based on the username, changing the value of the username can allow an attacker to impersonate other logged-in users. Likewise, if the isAdmin value is utilized for access control, an attacker can use it as a straightforward means of privilege escalation.
Accepting arbitrary signatures
JWT libraries usually offer two methods: one for verifying tokens and another for decoding them. In the case of the jsonwebtoken library for Node.js, the verify() method is used for verification, while the decode() method is used for decoding. However, sometimes developers mistakenly use only the decode() method, without verifying the token using the verify() method. This results in the application not verifying the signature at all, which can lead to potential vulnerabilities.
Accepting unsigned JWTs
The header of JWTs contains an "alg" parameter, which indicates to the server which algorithm was used during signing.
{
"alg": "HS256",
"typ": "JWT"
}
The flaw lies in the fact that the server's method for verifying the authenticity of the token can be manipulated by an attacker. While JWTs can be signed using various algorithms, they can also be left unsigned, which is referred to as an "insecure JWT" when the "alg" parameter is set to "none". Since unsigned tokens pose a significant risk, servers generally reject them. However, since the server's filter relies on string parsing, obfuscation techniques such as mixed capitalization and unexpected encodings can occasionally be used to circumvent these filters.
JWT header parameter injection
While the JWS specification only requires the "alg" header parameter, JWT headers, or JOSE headers, commonly include additional parameters. Attackers are particularly interested in three of these parameters: jwk (JSON Web Key), jku (JSON Web Key Set URL), and kid (Key ID). JWK provides a JSON object with an embedded key, while JKU provides a URL for the server to retrieve a set of keys containing the correct key. KID provides an ID that servers can use to identify the correct key when multiple keys are available. These user-controlled parameters inform the recipient server which key to use when verifying the signature.
Injecting self-signed JWTs via the jwk parameter
JWTs allow servers to embed their public key directly in the token itself in JWK format.
{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}
To ensure secure verification of JSON Web Tokens (JWTs), it is best practice for servers to limit the use of public keys to a specific whitelist. Nevertheless, some servers may not be configured correctly and accept any key included in the "jwk" parameter. This presents a vulnerability that hackers can take advantage of by signing a modified JWT with their own RSA private key and adding the corresponding public key to the jwk header.
Injecting self-signed JWTs via the jku parameter
Rather than directly embedding public keys through the jwk header parameter, certain servers permit the usage of the jku header parameter (JSON Web Key Set URL) to point to a JWK set that contains the required key. During signature verification, the server retrieves the pertinent key from the specified URL.
{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}
Sets of JWK like the one shown may be made public through a standard endpoint, such as /.well-known/jwks.json.
While more secure websites restrict key retrieval to trusted domains, attackers can still potentially bypass this filtering by exploiting gaps in URL analysis.
Injecting self-signed JWTs via the kid parameter
To sign various data types, not just JWTs, servers can utilize multiple cryptographic keys. To help the server identify the appropriate key for signature verification, the JWT header may contain a key ID (kid) parameter.
Usually, verification keys are kept in a JWK set, allowing the server to easily search for the JWK with the matching kid as the token. However, the JWS specification doesn't prescribe a specific structure for the ID, and it is merely an arbitrary string chosen by the developer. For instance, they might use the kid parameter to reference a particular database entry or even a filename.
If this parameter is susceptible to directory traversal attacks, a hacker could coerce the server into using a file from their filesystem as the verification key, opening up the possibility of serious security breaches.
{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}
When a server also allows JWTs to be signed using a symmetric algorithm, this becomes an especially risky situation. An attacker may manipulate the kid parameter to reference a predictable static file and then sign the JWT utilizing a secret that corresponds to the contents of that file. While this could potentially be done with any file, one of the easiest ways is to use /dev/null, a file that exists on most Linux systems, and returns null when retrieved as it's empty. Hence, signing the token using a null byte encoded in Base64 produces a valid signature.
How to prevent?
To safeguard your websites from the attacks we've discussed, implement the following high-level measures:
Ensure that you're using an up-to-date library for JWT management and that your developers understand its functionality and all security implications. While modern libraries reduce the risk of unintentionally implementing insecure measures, there is still potential for vulnerabilities due to the inherent flexibility of specifications. Perform robust signature verification on all JWTs received, taking into account extreme cases such as those signed using unexpected algorithms. Apply a strict whitelist of allowed hosts for the jku header to prevent unauthorized access. Ensure that the kid header parameter isn't vulnerable to path traversal or SQL injection. Although not essential for preventing vulnerabilities, we recommend adopting the following best practices when using JWTs in your applications:
Always set an expiration date for all issued tokens. Whenever possible, avoid sending tokens in URL parameters. Include the aud (audience) claim (or equivalent) to specify the intended recipient of the token and prevent it from being used on other websites. Allow the issuer server to revoke tokens, such as upon logout.