JWT Attacks: The Complete Practical Guide
JWT Header Injection: jwk, jku, kid, and x5u
How JWT header parameters that select the verifying key are abused: embedding your own jwk, hosting a jku JWKS, and kid path traversal or SQL injection.
Published
Several JWT header parameters exist to tell the server which key should verify the token. That is convenient for systems that rotate or hold many keys, and fatal when the server trusts the token to name its own key, because the attacker writes the header. This page covers the four that matter: jwk, jku, kid, and the certificate variants x5u/x5c. Practice on a free live target.
The core flaw: the token names its own key
Verification needs a key. A well-built server already knows which key to use and ignores whatever the token says. A vulnerable server reads a header parameter to decide, which means you can point it at a key you control and then sign the token with the matching private key. The server verifies happily, because by its own logic the signature is valid. Each parameter below is a different way to nominate that key.
jwk: embed your own public key
The jwk header can carry an entire public key inside the token. A server that trusts it will verify the token against the key you just handed it. So you generate your own key pair, put your public key in the jwk header, and sign the token with your private key:
{
"alg": "RS256",
"jwk": { "kty": "RSA", "n": "<your-modulus>", "e": "AQAB" }
}jwt_tool generates a key pair, injects the public half as a jwk, and signs for you:
python3 jwt_tool.py <TOKEN> -X i-X i is the inject-jwk exploit. The server verifies the token with the embedded key and trusts your claims.
jku: point the server at your JWKS
The jku header is a URL where the server fetches a JSON Web Key Set, a list of public keys. Trust it and you can host your own JWKS and reference it:
{
"alg": "RS256",
"jku": "https://attacker.example/jwks.json"
}You host a JWKS at that URL containing the public half of a key pair you own, then sign the token with the private half. jwt_tool automates the signing and JWKS referencing:
python3 jwt_tool.py <TOKEN> -X s -ju https://attacker.example/jwks.json-X s is the spoof-JWKS exploit and -ju sets the URL. In practice you also need to actually serve the JWKS at that address. This attack depends on the server fetching an attacker-supplied URL, so it often overlaps with SSRF-style controls, and a well-configured server restricts or pins where it will fetch keys from.
kid: path traversal and SQL injection
The kid (key id) header is a short identifier the server uses to look up a key it already holds. Unlike jwk and jku, you are not supplying the key, you are abusing the lookup. Two classic sinks:
Path traversal. If kid builds a filename, steer it to a file whose contents you know. The usual trick is a predictable empty file, then sign the token with an empty secret:
{ "alg": "HS256", "kid": "../../../../../../dev/null" }/dev/null reads as empty, so the server loads an empty key, and you sign your token with a matching empty or null key. The encoding detail is a property of your tool, not the server: in Burp's JWT editor the symmetric key goes in a JWK whose k field is base64url, so you enter AA== (base64url for a single null byte) there, while jwt_tool or raw code takes the empty or null key directly. Either way the server, having loaded "the key" from /dev/null, verifies with the same value and it matches.
SQL injection. If kid is looked up in a database, inject into the query so it returns a value you control, then sign with that value:
{ "alg": "HS256", "kid": "nonexistent' UNION SELECT 'attackerkey'-- -" }Here the lookup returns attackerkey, so you sign the token's HMAC with the secret attackerkey. The kid sink is really SQL injection wearing a JWT hat, and the same care with quoting and comment syntax applies.
x5u and x5c: the certificate variants
x5u and x5c are the certificate equivalents of jku and jwk. x5u is a URL to an X.509 certificate (or chain), x5c embeds the certificate in the header. The abuse is identical: point the server at or embed a certificate whose private key you hold:
{ "alg": "RS256", "x5u": "https://attacker.example/cert.pem" }Anywhere the server extracts a verification key from an attacker-supplied certificate parameter, you can supply your own.
Confirming and reading the result
As with every JWT forge, the signal is behavioural: the server now trusts the claims in a token you signed with your own key. If one parameter is ignored, try the next, since a server might pin the key for one of them but trust another.
Fixing it
- Do not trust token headers to select the key. Pin the verifying key server-side, or at most map a
kidthrough a strict allow-list of known ids, never a file path or a raw query. - Reject or pin
jwk,jku,x5u, andx5c. A session token has no business carrying or pointing at its own verification key. - Sanitise
kidif you must use it for lookup, treating it as untrusted input to whatever sink it reaches.
Full guidance is in preventing JWT attacks. Next, do all of this faster from the proxy in working with JWTs in Burp, or try a header injection on a free target.