What JWT Guides Don't Teach: Key Reference Injection as a Unified Attack Class
A Linktree user took full control of other accounts by manipulating JWT claims accepted without re-verification by backend services (HackerOne #1760403). The attack is not algorithm confusion. It is not a weak secret. It is attacker-controlled key reference. PortSwigger, Auth0, and OWASP document this class as 3 separate items without naming their common root.
Guides teach what was identified in 2015. The 2024-2025 bounties pay for something else.
Algorithm Confusion Gets the Attention: Key Reference Gets the Bounties
CVE-2025-30204 affects golang-jwt with CVSS 7.5. A malformed Authorization header triggers excessive memory allocation during JWT parsing with no authentication required, exploitable over the network at low complexity. The Red Hat advisory was published with no mitigation available at the time of disclosure.
CVE-2024-54150 affects the cjwt library in C. The cjwt_decode() function accepts the alg field declared in the token header without validating it against the algorithm the server expects. The attacker switches RS256 to HS256 and signs the token using the public key as the HMAC secret. The server successfully verifies a token it should never have accepted.
HackerOne #1760403 demonstrated complete account takeover at Linktree via JWT claim manipulation. HackerOne #1889161 showed the aud field accepted without validation by a widely used library. Attacker input passes directly into authorization logic. Both reports involve attacker input reaching JWT verification without sanitization.
The 2024-2025 pattern is consistent: CVEs and bounties concentrate on header parameter parsing and claim trust, not on weak secrets or brute force. The canonical Auth0 guide on JWT vulnerabilities dates from 2015. The OWASP JWT Cheat Sheet lists defensive bullets without an attack methodology. PortSwigger treats each vector as an isolated lab without a decision tree across classes.
Kid, JKU, and JWK Are Not Three Bugs: They Are One Attack Surface
All 3 share one root: the server resolves its verification key from an attacker-controlled header field. The distinction between them is the resolution mechanism, not the risk category.
The kid (Key ID) field points to which key the server uses. If resolution happens via SQL query, the attacker injects ' UNION SELECT 'known_secret'-- to control the value returned as the signing key. If resolution happens via filesystem, the payload ../../dev/null makes the server read an empty file. An HMAC computed over an empty string validates any forged token.
The jku (JWK Set URL) field instructs the server to fetch the public key from a URL. The attacker points jku to a server they control hosting a JWK Set with their own key pair. The server fetches the URL, uses the attacker's public key, and successfully verifies a token signed by the corresponding private key.
The jwk (JSON Web Key) field lets the attacker embed their public key directly in the header. The server uses that field to verify the signature without questioning the key's origin. The attacker generates an RSA key pair, inserts the public key in the jwk field, signs with the private key, and the token passes.
| Parameter | Value controlled by attacker | Exploit primitive |
|---|---|---|
kid |
Key ID mapped to SQL or filesystem | SQL injection or path traversal |
jku |
URL of a JWK Set under attacker control | External fetch to attacker key |
jwk |
Public key embedded in header | Token signed by attacker |
OWASP covers kid sanitization and the jku/jwk vectors as disconnected defensive bullets. No version of the Cheat Sheet names a unified attack category or describes a testing methodology that covers all 3 vectors.
The Fingerprinting Step That Turns Theory Into an Exploit
No major guide explains how to identify which key resolution mechanism a target uses before choosing the attack. This fingerprinting step separates finding a JWT bug from wasting hours against the wrong surface.
A JWT header is base64url-encoded without a signature. Any simple decode exposes the present fields without any cryptographic key. The first step is always decoding the header and recording all parameters: alg, kid, jku, jwk, x5u, x5c.
With kid present, the target resolves keys by key ID. Testing kid: "../../dev/null" reveals path traversal; testing kid: "' OR '1'='1--" reveals SQL injection. With jku present, setting up an endpoint in Burp Collaborator or interactsh confirms whether the server makes an external fetch before any key substitution. With jwk present, generating an RSA key pair and embedding the public key in the header is the immediate test.
With none of those fields present, the configuration uses a static key. Kid, jku, and jwk do not apply. The relevant attacks in that case are alg:none and RS256-to-HS256 confusion. PortSwigger maintains 8 JWT labs without a decision tree between them: each lab assumes the target mechanism is already known to the tester. The jwtXploiter tool implements all attack classes but does not automate the fingerprinting logic that determines which one to use.
B2B APIs and API Gateways Have Structural Exposure to Key Reference
Multi-tenant applications with distinct signing keys per client must dynamically resolve which key to use. The kid field in the header is the most common mechanism for that routing. An attacker with 2 legitimate accounts can probe kid manipulations across tenant boundaries without any privileged access.
B2B OIDC integrations that accept JWTs from third parties fetch keys via jwks_uri. The jku-class injection surface is available at every external identity integration point. API gateways validating JWTs from multiple issuers cannot use a static key: dynamic resolution is mandatory. Dynamic resolution is the prerequisite for all 3 injection vectors.
SAML-to-JWT bridges often pass kid values from the SAML assertion directly into the generated JWT without sanitization. HackerOne #1210502 (Jitsi-Meet) demonstrated structural exposure by accepting JWTs from external issuers without fixing the algorithm the server expects. The pattern of accepting external tokens with a negotiable algorithm creates the same surface that kid/jku/jwk injection exploits internally.
The jwt_scanner spell on intel.mago.team (MAGO team tool) automates header fingerprinting across all JWT endpoints mapped in the API surface. The output classifies each endpoint by its key resolution mechanism before any injection attempt, eliminating the manual work of step 2.
A Three-Step Decision Tree for Testing Key Reference Injection
With any JWT-protected endpoint, the complete flow takes under 10 minutes without specialized tools. The structure is fixed: decode, fingerprint, inject.
Step 1: Decode. Extract all JWTs from requests and responses. Decode the header via base64url without cryptographic verification. Record all parameters present in the header.
Step 2: Fingerprint. Classify the endpoint by header content:
kid present -> SQL injection: kid = "' UNION SELECT 'secret'--"
path traversal: kid = "../../dev/null"
jku present -> confirm OOB fetch (Collaborator/interactsh)
host own JWK Set, substitute URL in jku field
jwk present -> generate RSA key pair
insert public key in header jwk field
sign token with corresponding private key
none present -> test alg:none (remove signature)
test RS256 -> HS256 confusion with public key as secret
Step 3: Inject. Execute the primitive that matches the fingerprint. HTTP 200 with another user's data confirms full bypass. HTTP 401 with "invalid signature" confirms algorithm enforcement. HTTP 400 with an error on the kid field confirms the injection surface without an immediate bypass.
This decision tree does not appear in any major JWT guide. PortSwigger, Auth0, and OWASP document each attack class without a methodology that crosses between classes.
The difference between a JWT that passes tests and one that pays a bounty is in the header fingerprint. The attack is in the field the guide calls "optional."
Top comments (0)