# 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:

```json
{
  "alg": "RS256",
  "typ": "JWT"
}
```

**Payload** — arbitrary claims. Some are registered (`iss`, `exp`, `sub`), others are application-specific:

```json
{
  "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

```json
{
  "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](https://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:

```bash
# 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:
   ```bash
   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](https://github.com/latchset/jwcook) or the Python `jwcrypto` library can do this:
   ```python
   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](https://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

### 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](https://github.com/ngnisp/sig2n) |
| **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:

1. Collect two JWTs from the target (login twice).
2. Use the [sig2n](https://github.com/ngnisp/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](https://gchq.github.io/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
   # 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.

   ```python
   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:
   ```python
   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.

| 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](https://jwt.io) — Inspect and forge JWTs
- [jwt_tool](https://github.com/ticarpi/jwt_tool) — Swiss Army knife for JWT testing
- [sig2n (ngnisp/sig2n)](https://github.com/ngnisp/sig2n) — Public key recovery from JWTs
- [RFC 7515 — JSON Web Signature](https://datatracker.ietf.org/doc/html/rfc7515)
- [RFC 7517 — JSON Web Key](https://datatracker.ietf.org/doc/html/rfc7517)
- [RFC 7519 — JSON Web Token](https://datatracker.ietf.org/doc/html/rfc7519)
- [RFC 7518 — JSON Web Algorithms](https://datatracker.ietf.org/doc/html/rfc7518)
- [CyberChef](https://gchq.github.io/CyberChef/) — JWT signing and decoding recipe

