| Field | Detail |
|---|---|
| Platform | PortSwigger Web Security Academy |
| Type | JWT Attacks |
| Difficulty | Practitioner |
| Objective | Inject a self-signed JWT using the jwk header parameter to impersonate the administrator and delete carlos |
JWT Authentication Bypass via JWK Header Injection¶
I logged in as wiener:peter and decoded the session JWT:
Header:
{"kid":"fc46afdb-7d08-4f8c-ac63-a5c6b215e68d","alg":"RS256"}
Payload:
{"iss":"portswigger","exp":1783042462,"sub":"wiener"}
alg: RS256 — asymmetric. Without the server's private key, forging a signature normally isn't possible. But the server supports the jwk header parameter, which changes the situation.
RS256 signs with a private key and verifies with the public key. The jwk header parameter was designed to let servers embed the public key directly in the token for federated identity scenarios. The flaw: if the server uses whatever public key is in the jwk field without checking whether that key came from a trusted source, we can supply our own key pair. We sign the modified JWT with our private key, embed our public key in the jwk header, and the server verifies against the key we provided — which passes. The cryptographic verification is sound; the trust chain is broken because the server never checked it owned that key. The jwk header turns key verification into a self-referential loop: the server is asked to verify a token using the key the token itself provides.
I generated a new RSA key pair in Burp's JWT Editor Keys tab:
In Repeater's JSON Web Token tab, I changed sub to administrator:
Then clicked "Attack" → "Embedded JWK" and selected the generated key. Burp's extension handles the three steps that need to stay consistent: it updates the kid in the header to match the embedded key, encodes the public key in JWK format, and re-signs with the private key. Doing this manually is error-prone if any step is out of sync.
The resulting header embedded our public key in the jwk field:
{
"kid": "eebeae67-b929-46f9-a457-e48a160945c8",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "eebeae67-b929-46f9-a457-e48a160945c8",
"n": "s6pgPehsPomnTsjEP19FjzdV..."
}
}
Unlike the HS256 brute-force lab, there was no secret to recover — we generated our own key pair from scratch and the server accepted it. The attack works even against a server with a perfectly strong RS256 private key. The fix is a strict key whitelist: the server should only accept jwk values matching trusted public keys, or reject the parameter entirely and always look up keys internally.
I pasted the forged token into the browser's session cookie storage:
Admin panel loaded. Deleted carlos:
Lab solved