ASVS 5.0 V9.1.3 requires that the key material used to validate self-contained tokens comes from trusted, pre-configured sources for the token issuer. Attackers must not be able to name their own keys. For JWTs, headers such as jku, x5u and jwk must be checked against an allowlist of trusted sources. It is a Level 1 requirement.
Why it matters
Some JWT headers say where to find the verification key. If your verifier follows jku to any URL, the attacker hosts a key set, signs their token with the matching private key and the token verifies. The same holds for an embedded jwk or an x5u certificate URL. You end up verifying the signature against a key the attacker supplied.
How to implement it
Configure the key source per issuer in your own settings. Ignore key location headers by default.
import { createRemoteJWKSet, jwtVerify, decodeProtectedHeader } from "jose";
const TRUSTED = new Map([
["https://idp.example.com/", createRemoteJWKSet(new URL("https://idp.example.com/.well-known/jwks.json"))],
]);
export async function verify(token: string, expectedIssuer: string) {
const header = decodeProtectedHeader(token);
if (header.jku || header.x5u || header.jwk) throw new Error("key location headers not accepted");
const jwks = TRUSTED.get(expectedIssuer);
if (!jwks) throw new Error("unknown issuer");
return jwtVerify(token, jwks, { issuer: expectedIssuer, algorithms: ["RS256"] });
}
Notice that the issuer is chosen from your own routing (the endpoint or tenant configuration), not read out of the unverified token. If you must support several issuers, key the lookup on a value you control. If a jku is genuinely needed, match its full URL exactly against an allowlist and fetch over HTTPS only.
Use kid only to select among keys inside the trusted set.
How to test it
- Host a JWKS on a server you control, sign a token with that key and set
jkuto your URL. Expect rejection and no outbound request to your server in its logs. - Embed your public key in a
jwkheader and sign with it. Expect rejection. - Set
x5uto your certificate URL. Expect rejection. - Send an unknown
kid. Expect rejection, without a fetch loop to the issuer.
Common mistakes
- Following
jkuwith a startswith check, whichhttps://idp.example.com.evil.testpasses. - Selecting the issuer from the unverified
issclaim and fetching keys from wherever that points. - Fetching a JWKS on every unknown
kid, which lets attackers cause traffic and denial of service. - Trusting a key set over plain HTTP.
Quick check
Is a startswith check on the jku URL safe?
No. Prefixes can be extended into attacker domains. Compare the whole URL exactly against an allowlist.