A JSON Web Token carries a header, and that header is allowed to tell the server where to find the key that checks the token. JWT jku header injection is what happens when a server believes that instruction. The attacker signs a token with a private key they generated this morning, points the jku field at a key file they host, and the server dutifully fetches it and confirms the signature is valid. It really is valid. That is the problem.
What is the jku header supposed to do?
The jku parameter is a URL that points at a JWK Set, a JSON document listing the public keys an issuer signs with. RFC 7515 defines it so that a verifier which does not already hold the issuer’s key can go and get it. The idea sounds reasonable in a federated world where many issuers rotate keys on their own schedule and nobody wants to redeploy a config file every ninety days.
A token is three base64url segments joined by dots: header.payload.signature. A header carrying a key location looks like this:
header
{
"alg": "RS256",
"typ": "JWT",
"kid": "acme-2026-04",
"jku": "https://auth.acme.example/.well-known/jwks.json"
}
payload
{ "sub": "1042", "role": "user", "exp": 1893456000 }
And the JWKS at that URL holds the matching public key:
{
"keys": [
{ "kty": "RSA", "kid": "acme-2026-04", "use": "sig",
"n": "0vx7agoebGcQSuu...", "e": "AQAB" }
]
}
Here is the detail everything else follows from. The signature covers the header and the payload. It cannot cover the key, because the key is the thing doing the covering. So jku sits in a segment the attacker can rewrite freely, and rewriting it does not break anything the verifier would notice.
How JWT jku header injection actually works
The attack is four steps and needs no cryptographic weakness at all. Picture a fictional app, Acme Notes, whose API gateway reads jku and fetches it.
- Generate a key pair. The attacker makes their own RSA key pair on a laptop. Nothing about it is secret from them, which is the entire point.
- Publish the public half. They write a JWKS file containing that public key, give it a
kid, and host it at a URL they own. - Forge the claims. They take a real token, change
"role": "user"to"role": "admin", and setjkuto their own URL. - Sign it. They sign the forged token with their private key and send it to Acme Notes.
The gateway decodes the header, reads jku, makes an outbound request, parses the JWKS, matches on kid, and runs the RSA verification. It succeeds, because the token was signed by the private half of exactly that public key. The gateway logs a successful verification from a trusted algorithm and admits an administrator who does not exist.
attacker sends
header { "alg": "RS256", "kid": "evil-1",
"jku": "https://attacker.example/jwks.json" }
payload { "sub": "1", "role": "admin" }
signature = RSA_sign(header.payload, attacker_private_key)
Note what did not happen. Nobody broke RS256. Nobody stole Acme’s private key. The algorithm stayed honest the whole way through. Only the answer to “which key” moved, and that answer came from the token.
Why does a passing signature mean nothing here?
Because a signature check answers a narrower question than most people read into it. It answers: was this data signed by the holder of the private key matching the public key I was given. It does not answer: is that public key one I should trust. Those are two separate decisions, and only the second one is authentication.
Verifying a signature against a key the attacker chose is like checking a passport against a stamp the traveller brought with them.
This is the same shape as SAML signature wrapping, where the cryptography is also flawless and the failure is that the verified thing and the trusted thing are not the same thing. In both cases the audit log looks clean, because from the code’s point of view nothing went wrong.
The jwk and x5u headers are the same bug in different clothes
Two sibling header parameters fail the same way, and a fix that only closes jku leaves the door open.
jwkembeds the public key directly in the header instead of linking to it. The attacker does not even need to host a file. They paste their own public key into the token and sign with the matching private key. If the verifier uses the embedded key, every token becomes self certifying.x5upoints at a URL holding an X.509 certificate or chain. Same fetch, same attacker controlled destination, just wrapped in certificate format. A server that walks the chain without checking it terminates at a certificate authority it actually trusts is in the same position.
RFC 7515 does say a verifier using jku or x5u must fetch over TLS. That helps nothing here. TLS proves you reached the host named in the URL. The attacker owns that host, so the certificate is genuine and the connection is honest. Transport security cannot rescue a trust decision that was wrong before the request went out.
How do you prevent this attack?
The short version: the verifier decides which key to use, never the token. Everything below is a way of enforcing that.
- Pin the JWKS URL in server configuration. Your service should already know its issuer’s key endpoint, fetch it on its own schedule, and cache the result. There is no reason to read a location out of client input.
- Reject tokens that carry
jku,jwk, orx5uat all. If your design does not use these parameters, their presence is a signal worth alerting on, not a field to ignore quietly. - Allowlist exact URLs if you truly need dynamic issuers. Compare the full URL, scheme, host and path, against a short fixed list before any fetch. Host only matching gets defeated by an open redirect or a URL parser disagreement.
- Validate
kidagainst a known key set. Treat it as an index into a table of keys you already hold, not as a filename or a query fragment. Unknown id means reject, not go looking. - Pin the algorithm too. Pass an explicit allow list such as
["RS256"]to the verify call so a swappedalgis refused before any key lookup starts. - Test it directly. Send a token with
jkupointed at a host you control and see whether an outbound request arrives. That single observation tells you everything.
Those last two overlap with a related family worth understanding separately. Lying about the algorithm, through alg set to none or an RS256 token replayed as HS256, is covered in our post on JWT algorithm confusion. One bug lies about how the token is checked. The one here lies about what it is checked against. A service can have either, both, or neither, so test them as separate questions.
The assumption underneath
Every one of these header parameters exists because a spec author imagined a cooperative issuer supplying helpful metadata. That assumption is invisible in the code. What a reviewer sees is a verify call with a real algorithm and a real signature, and it looks finished. The question nobody writes down is where the key came from and who chose it, which is exactly the kind of assumption an autonomous researcher built to probe an app’s beliefs, rather than replay known payloads, is meant to surface and then prove with evidence. More on that approach on our about page.
Frequently asked questions
What is JWT jku header injection?
It is an attack where a token’s jku header names the URL a server fetches its verification key from. The attacker hosts a JWK Set containing their own public key, signs the token with the matching private key, and the server verifies it successfully against a key the attacker chose.
Why does the signature check still pass?
A signature check only proves the data was signed by whoever holds the private key matching the public key supplied to the verifier. It says nothing about whether that public key belongs to a trusted issuer. When the token picks the key, a passing check proves only that the attacker can sign their own tokens.
How are the jwk and x5u headers related?
They are the same failure in a different format. The jwk parameter embeds a public key directly in the header, so the attacker does not even need to host a file, and x5u points at a URL holding an X.509 certificate chain. A fix that closes only jku leaves both of these open.
How do you prevent it?
Never take key material from the token. Pin the JWKS URL in server configuration and fetch it on your own schedule, reject tokens carrying jku, jwk, or x5u if you do not use them, allowlist exact URLs if you must be dynamic, validate kid against a known key table, and pin the accepted algorithms on the verify call.
Put an autonomous researcher on your own systems
UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.
