JSON Web Token Security
In the realm of web development, securing communications between clients and servers is crucial. JSON Web Tokens (JWT) have emerged as a popular method for achieving this, thanks to their compact and self-contained format for securely transmitting information as JSON objects. However, while JWTs offer a robust framework for authentication and information exchange, they also come with security considerations that developers must address. In this blog post, we’ll explore the details of JWT security, highlighting best practices for implementation and common vulnerabilities to watch out for.
What is a JWT?
Imagine you’re sending a sealed letter that not only contains a message but also has a special stamp on it verifying that it’s genuinely from you and hasn’t been tampered with. That’s essentially what a JWT does in the digital world. It’s a compact string of characters that securely carries information between two entities—be it two servers, or a server and a client—in a way that can be verified and trusted.
Structure of a JWT
A JWT is made up of three parts: the header, the payload, and the signature. Each part has its role and together, they form a powerful tool for secure communication.
- Header: The header typically consists of two parts: the type of token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA. This part is like the envelope of our letter, telling you what kind of message it is and how the seal (signature) is made.
- Payload: This section contains the actual data (the message in our letter analogy). It includes claims, which are statements about an entity (usually the user) and additional data. There are three types of claims: registered, public, and private. Registered claims are predefined (like an issued date), public claims can be defined by those using JWTs, and private claims are used to share information between parties that agree on them.
- Signature: To create the signature, you take the encoded header, the encoded payload, a secret, and sign that with the algorithm specified in the header. This signature is like the wax seal on our letter, ensuring that the message hasn’t been altered in transit and verifying the sender’s identity.
The combination of these three parts, separated by dots (xxxxx.yyyyy.zzzzz
), forms a complete JWT.
How Does JWT Work?
When a user logs in to a system, a JWT can be generated by the server, signed, and sent back to the user. The user then stores this token and sends it along with every subsequent request to the server. The server, upon receiving the token, decodes it, verifies the signature, and if everything checks out, processes the request. Because the token contains all the necessary information, the server doesn’t need to query the database more than once, making JWTs not just secure but really efficient.
Why Use JWTs?
Security: JWTs offer a secure way to transmit data between parties. The information is encrypted and the signature ensures that it hasn’t been altered.
Efficiency: Since the token contains all the necessary information, it reduces the need to make database calls, speeding up the application.
Scalability: JWTs are stateless. The server doesn’t need to keep a record of tokens. This makes scaling applications easier because it lessens the load on the server.
Best Practices for JWT Security
Implementing JWTs securely requires adherence to several best practices:
1. Use HTTPS
Always use HTTPS to protect JWTs in transit. This prevents token interception and ensures the integrity of the security token as it flows between the client and server.
2. Keep it Short-Lived
The longer a token is valid, the higher the risk of it being misused. Implement short expiration times for tokens and consider using refresh tokens to maintain sessions over longer periods securely.
3. Secure Token Storage
On the client side, store JWTs securely to prevent Cross-Site Scripting (XSS) attacks. Avoid storing tokens in local storage and prefer HTTPOnly cookies or other secure mechanisms.
4. Validate Input
Always validate JWTs to ensure they conform to the expected format and contain all necessary claims. This helps in preventing injection attacks and other forms of tampering.
5. Use Strong Keys and Algorithms
For signing JWTs, use strong keys and secure algorithms. Prefer asymmetric algorithms like RSA or ECDSA for public applications, where the signing key is public and the verification key is kept private.
Common Vulnerabilities
Despite their advantages, JWTs are subject to several vulnerabilities if not properly handled:
1. Weak Keys
Weak or easily guessable keys can lead to JWT forging. Ensure that keys are complex and securely stored.
2. None Algorithm Vulnerability
Some implementations may accept the "alg": "none"
in the header, which can bypass signature verification. Ensure your implementation does not trust tokens with none as the algorithm.
3. Signature Stripping
An attacker might modify the JWT header to indicate that the token is unsigned. Always verify that the token’s algorithm matches the expected algorithm.
4. XSS and CSRF Vulnerabilities
Improper token storage can make JWTs vulnerable to XSS attacks, while CSRF attacks can exploit authenticated sessions. Implement appropriate countermeasures, such as using secure cookies and CSRF tokens.
Example of the None Algorithm Vulnerability
The following is an example of a JWT with the "alg": "none"
header, which can bypass signature verification:
Lets say we have the following encoded JTW:
Using jwt.io, we have the capability to decode the token and examine its header and payload components in detail.
Header
{
"alg": "HS256",
"typ": "JWT"
}
Payload
{
"id": "1",
"name": "John Doe",
"role": "user",
"iat": 1707961757
}
The "alg": "HS256"
indicates that the token is signed using the HMAC SHA256 algorithm. However, if an attacker modifies the header to "alg": "none"
, the token can bypass signature verification and be accepted as valid.
We simply have to encode the following header in base64:
{
"alg": "None"
}
We can use base64encode to get the corresponding base64: ewoiYWxnIjogIk5vbmUiCn0=
. Then we can replace the original header(red field) with the newly encoded header.
In case some changes to the payload are needed, we can use base64decode to decode the payload, make the changes and then encode it again. In this case I simply changed the users role from user
to admin
making John a superuser.
{
"id": "1",
"name": "John Doe",
"role": "admin",
"iat": 1707961757
}
Next, I replaced the original payload with the new one.
As no algorithm is used to sign the token, the signature can be removed and the token would still be accepted as valid in a vulnerable system.
This could theoretically work as we thereby are bypassing the signature verification. So again, ensure your implementations and don’t trust tokens with none as the algorithm.
And just like that, we have successfully exploited the None Algorithm Vulnerability making our user John Doe a superuser.