Exploiting JSON Web Key (JWK)
JWTs are everywhere. They are the stateless session token of choice for modern web applications. But with great convenience comes a long tail of misconfigurations that turn a robust cryptographic standard into a wide-open door.
This post explores one of the more elegant attacks in the JWT ecosystem: JWK injection. By the end, you will understand how a misplaced trust in the jwk header parameter can let an attacker forge tokens at will, and — more importantly — how to shut it down.
The Vocabulary Section
Before we dive into exploitation, let us establish a shared lexicon:
- Authentication: Confirming a user’s identity (“who you are”).
- Authorization: Verifying a user’s access level (“what you can do”). Common models include DAC, MAC, RBAC, and ABAC.
- OAuth 2.0: An open standard protocol for secure authorization delegation between services without sharing credentials.
- SAML: An XML-based standard for exchanging authentication and authorization data between Identity Providers (IdPs) and Service Providers (SPs). Enables SSO through digitally signed assertions.
- Claim: A key-value pair inside a JWT payload (e.g.,
"isAdmin": true).
A JWT consists of three base64url-encoded segments:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyIjoiYWRtaW4iLCJpc0FkbWluIjp0cnVlfQ.
MHYDlQp5fGzJ0bCwz5qF_v8n9WxKj...signatureHeader — metadata about the token:
{
"alg": "RS256",
"typ": "JWT"
}Payload — arbitrary claims. Some are registered (iss, exp, sub), others are application-specific:
{
"iss": "example-app",
"user": "admin",
"isAdmin": true,
"exp": 1782164173
}Signature — computed over header.payload using the algorithm and key specified in the header.
The integrity of the entire token hinges on this signature. If an attacker can forge a valid signature, they can set isAdmin: true, impersonate any user, and effectively own the application.
Attack Vector: JWK Injection
The jwk header parameter is an optional field that carries the public key used to sign the JWT, expressed as a JWK object. It is intended for scenarios where the verifier does not have a pre-configured key and must derive it from the token itself.
Here is the problem: if the server naively accepts whatever key the token presents, an attacker can generate their own key pair, embed the public key in the jwk header, sign the payload with their private key, and the server will happily verify it.
It is the equivalent of letting a visitor stamp their own passport and then accepting it without checking who issued it.
The Anatomy of a JWK
{
"kty": "RSA",
"n": "0vx7ago...T4Q",
"e": "AQAB",
"alg": "RS256"
}kty: Key type (RSA,EC,oct)n: RSA modulus (base64url encoded)e: RSA public exponent (base64url encoded)
When a server sees a JWT with a jwk header, it extracts this key and uses it to verify the signature. If there is no validation that the key comes from a trusted source, you have a vulnerability.
Detection
- Obtain a valid JWT from the target application (register/login).
- Decode it at jwt.io and inspect the header.
- Look for the presence of
jwk,jku(JWK Set URL), orkid(Key ID) parameters. - Check if the server accepts tokens where these parameters are modified.
Exploitation
The jwt_tool toolkit provides a straightforward way to test and exploit this:
# Inline JWKS injection (-X i flag)
python3 jwt_tool/jwt_tool.py -X i -pc isAdmin -pv true \
-I eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...This generates a new RSA key pair, injects the public key as a JWK in the header, signs the tampered payload with the private key, and outputs a fully forged token.
Manual Exploitation
For a deeper understanding, here is the manual process:
Generate an RSA key pair:
openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pemConstruct a JWK from the public key. Tools like jwcook or the Python
jwcryptolibrary can do this:from jwcrypto import jwt, jwk key = jwk.JWK.from_pem(open('private.pem').read()) # key.export() outputs the public JWKForge the JWT with the JWK header at jwt.io:
- Set algorithm to
RS256 - Paste your private key
- Add the
jwkheader with your public JWK - Set the payload claims you want
- Set algorithm to
Submit the tampered token to the target. If the server accepts it, you have confirmed the vulnerability.
A quick sanity check at jwt.io during testing — decode your forged token there first to verify the structure before sending it to the target.
Real-World Impact
Consider a company hosting multiple microservices, all sharing the same JWT validation logic. An attacker obtains a low-privilege token from Service A, forges a JWK-injected token with admin privileges, and uses it against Service B — which handles sensitive operations. The blast radius extends across the entire ecosystem.
This is especially dangerous when:
- The same validation library is used across services
- Privilege levels are encoded in JWT claims (they should not be)
- No JWK whitelist or key pinning is in place
- The
jku(JWK URL) parameter is also accepted, enabling SSRF
Related Attacks
For completeness, here are the siblings of JWK injection:
| Attack | Mechanism | Tooling |
|---|---|---|
| None Algorithm | Set "alg": "none" — server skips signature verification | jwt_tool -X a |
| Algorithm Confusion | Swap asymmetric (RS256) to symmetric (HS256); sign with the public key | jwt_tool -X k -pk public.pem |
| Key Confusion via sig2n | Recover the public key from two JWTs to enable HS256 confusion | sig2n Docker |
| JWK Injection | Inject a self-generated public key into the jwk header | jwt_tool -X i |
| Blank Signature | Null or empty signature accepted by broken parsers | jwt_tool -X n |
Algorithm Confusion in Practice
If your target uses RS256, here is the workflow to test for confusion:
- Collect two JWTs from the target (login twice).
- Use the sig2n tool to recover the public key by providing both JWTs.
- Test candidate keys against the target to find the valid one.
- With the valid key, forge a token signed with HS256 using the base64-decoded public key as the HMAC secret. Tools like CyberChef with the JWT Sign recipe can craft the final payload.
Prevention
Never trust client-supplied keys. Your JWT validation should use a pre-configured key or fetch it from a trusted source (e.g., an OAuth provider’s JWKS endpoint). Reject tokens that carry a
jwkparameter unless you explicitly need it and validate the key against an allowlist.Pin the algorithm. Do not derive the algorithm from the token header alone. Configure your validation library to expect a specific algorithm:
# Python (PyJWT) — explicit algorithm, no surprises import jwt decoded = jwt.decode(token, public_key, algorithms=["RS256"])Validate the JWK if you must use it. If your architecture genuinely requires inline JWKs, verify that the supplied key matches a known fingerprint or is signed by a trusted CA.
from jwcrypto import jwt, jwk try: key = jwk.JWK.from_json(token_header["jwk"]) # Verify key fingerprint against whitelist if key.thumbprint() not in ALLOWED_KEY_THUMBPRINTS: raise ValueError("Untrusted key") except Exception: # Fall back to pre-configured key key = TRUSTED_PUBLIC_KEYWhitelist JKU origins. If you support
jku(JWK Set URL), maintain a strict allowlist of allowed hosts to prevent SSRF:ALLOWED_JKU_HOSTS = ["auth.example.com"]Always set an expiration (
exp) to limit the window of a stolen or forged token.Use established libraries. Do not write your own JWT parsing logic. Libraries like
PyJWT,jose,jsonwebtoken(Node.js), andjjwt(Java) are battle-tested. Keep them updated.Audit your configuration. Document exactly which algorithms, claims, and key sources your application supports. This configuration should be code-reviewed just like any other security-critical component.
Summary
JWK injection exploits a fundamental trust assumption: that the key presented in the token header is authentic. The fix is simple — validate keys against a known source, pin your algorithm, and never let the token define the terms of its own verification.
| Attack | Root Cause | Fix |
|---|---|---|
| None algorithm | Server accepts alg: none | Reject none, pin algorithm |
| Algorithm confusion | Server uses same key variable for sym/asym | Pin algorithm, use separate key storage |
| JWK injection | Server trusts inline public key | Whitelist keys, fetch from trusted JWKS |
| JKU SSRF | Server fetches keys from arbitrary URLs | Whitelist allowed JKU hosts |
References
- jwt.io Debugger — Inspect and forge JWTs
- jwt_tool — Swiss Army knife for JWT testing
- sig2n (ngnisp/sig2n) — Public key recovery from JWTs
- RFC 7515 — JSON Web Signature
- RFC 7517 — JSON Web Key
- RFC 7519 — JSON Web Token
- RFC 7518 — JSON Web Algorithms
- CyberChef — JWT signing and decoding recipe