Contents

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...signature

Header — 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

  1. Obtain a valid JWT from the target application (register/login).
  2. Decode it at jwt.io and inspect the header.
  3. Look for the presence of jwk, jku (JWK Set URL), or kid (Key ID) parameters.
  4. 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:

  1. Generate an RSA key pair:

    openssl genrsa -out private.pem 2048
    openssl rsa -in private.pem -pubout -out public.pem
  2. Construct a JWK from the public key. Tools like jwcook or the Python jwcrypto library can do this:

    from jwcrypto import jwt, jwk
    key = jwk.JWK.from_pem(open('private.pem').read())
    # key.export() outputs the public JWK
  3. Forge the JWT with the JWK header at jwt.io:

    • Set algorithm to RS256
    • Paste your private key
    • Add the jwk header with your public JWK
    • Set the payload claims you want
  4. 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

For completeness, here are the siblings of JWK injection:

AttackMechanismTooling
None AlgorithmSet "alg": "none" — server skips signature verificationjwt_tool -X a
Algorithm ConfusionSwap asymmetric (RS256) to symmetric (HS256); sign with the public keyjwt_tool -X k -pk public.pem
Key Confusion via sig2nRecover the public key from two JWTs to enable HS256 confusionsig2n Docker
JWK InjectionInject a self-generated public key into the jwk headerjwt_tool -X i
Blank SignatureNull or empty signature accepted by broken parsersjwt_tool -X n

Algorithm Confusion in Practice

If your target uses RS256, here is the workflow to test for confusion:

  1. Collect two JWTs from the target (login twice).
  2. Use the sig2n tool to recover the public key by providing both JWTs.
  3. Test candidate keys against the target to find the valid one.
  4. 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

  1. 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 jwk parameter unless you explicitly need it and validate the key against an allowlist.

  2. 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"])
  3. 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_KEY
  4. Whitelist 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"]
  5. Always set an expiration (exp) to limit the window of a stolen or forged token.

  6. Use established libraries. Do not write your own JWT parsing logic. Libraries like PyJWT, jose, jsonwebtoken (Node.js), and jjwt (Java) are battle-tested. Keep them updated.

  7. 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.

AttackRoot CauseFix
None algorithmServer accepts alg: noneReject none, pin algorithm
Algorithm confusionServer uses same key variable for sym/asymPin algorithm, use separate key storage
JWK injectionServer trusts inline public keyWhitelist keys, fetch from trusted JWKS
JKU SSRFServer fetches keys from arbitrary URLsWhitelist allowed JKU hosts

References