Safeguard
Application Security•

ASVS 5.0 V9.1.3: Use Only Trusted Sources for Token Keys

ASVS V9.1.3 (Level 1): signing keys come from pre-configured trusted sources. Ignore jku, x5u and jwk headers unless they match an allowlist.

Safeguard Security Team
3 min read

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

  1. Host a JWKS on a server you control, sign a token with that key and set jku to your URL. Expect rejection and no outbound request to your server in its logs.
  2. Embed your public key in a jwk header and sign with it. Expect rejection.
  3. Set x5u to your certificate URL. Expect rejection.
  4. Send an unknown kid. Expect rejection, without a fetch loop to the issuer.

Common mistakes

  • Following jku with a startswith check, which https://idp.example.com.evil.test passes.
  • Selecting the issuer from the unverified iss claim 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.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.