Sessions and Tokens — From the Browser to the Mesh
Don't let the token tell you how to verify it
In one line
A JWT means the token carries its own evidence. In exchange for being able to just verify the signature without asking the server, if you write that verification even a little loosely, the verifier follows whatever the token itself tells it about "how to verify me", and a forgery gets through. That looseness is alg confusion and kid injection.
Why this was needed
One JWT is three pieces split by two dots — the header, the payload (claims), and the signature. The header holds alg (the signing algorithm) and often kid (the key identifier). The verifier reads the header's alg, says "ah, it's RS256", and verifies the signature that way. The problem is that both that alg and that kid are values that have not been verified yet and that the attacker wrote however they liked.
Two classic attacks come from here. First, RFC 7519 §6 allows, by specification, an unsecured token (an Unsecured JWS) whose alg value is "none". If the verifier takes the header's alg at face value, the attacker puts the claims they want in alg:none, leaves the signature piece empty, and throws it over — the verifier says "it's none, so there is no signature to verify" and lets it through. The second is alg confusion. It is just as RFC 8725 puts it — "'RS256' parameters can be altered to 'HS256', leading libraries to validate using the RSA public key as an HMAC secret." RS256 is asymmetric so it is verified with the public key, and the public key is known to everyone (it is published through a JWKS). The attacker changes the header alg to HS256 and signs directly, using the public key string as the HMAC secret. If the verifier trusts the header's alg and says "it's HS256, so verify the HMAC with that key", the verification passes because that key is the public key. A valid signature was forged from a published key alone.
How it works
The heart of the defense is one sentence of RFC 8725 §3.1 — "Libraries MUST enable the caller to specify a supported set of algorithms and MUST NOT use any other algorithms." And "each key MUST be used with exactly one algorithm, and this MUST be checked."
Turning this into code gives three rules.
허용 alg 화이트리스트 검증기가 받아들일 alg 를 코드에 고정한다({RS256, HS256}).
none 은 목록에 없으니 자동으로 거절된다.
alg 를 키에 고정 헤더의 alg 를 믿지 않는다. kid 로 키를 찾고, 그 키의
타입(RSA→RS256, oct→HS256)이 정한 alg 로만 검증한다.
rs-1 은 RSA 키라 RS256 으로만 쓴다 — HS256 을 요구하면
서명을 보기도 전에 거절한다. 이것이 혼동을 막는다.
kid 는 조회 키일 뿐 kid 를 파일 경로나 URL 로 해석하지 않는다. 고정된 키
목록의 딕셔너리 키로만 쓰고, 없는 kid 는 거절한다.
Why be this careful about kid? RFC 7515 §4.1.4 says "The structure of the 'kid' value is unspecified" — meaning any string can come. If the verifier takes kid and reads a key file with open(kid) or pulls a URL with fetch(kid), the attacker puts their own key file path or their own server URL in kid and specifies the very key to be used for verification. RFC 8725 §3.10 warns of this path injection (and of SSRF through jku and x5u). kid must be only a handle for choosing one of the keys I know.
Verifying the signature is not the end. You have to look at the claims of RFC 7519 — exp (reject after this time), nbf (reject before this time), iss (is the issuer one I know), and especially aud. The RFC insists: "If the principal processing the claim does not identify itself with a value in the 'aud' claim when this claim is present, then the JWT MUST be rejected". This is because my API must not accept a token issued for another API. For clock skew you allow only a leeway of a few minutes at most.
The reason for having several keys and the legitimate use of kid are in RFC 7517 §4.5 — "to choose among a set of keys within a JWK Set during key rollover." When you change the signing key, tokens signed with the old key are still in circulation, so you keep both the old and new keys in the JWKS and pick with kid to verify. If you delete the old key too early, tokens that are still valid get rejected all at once.
What it looks like in the field
alg confusion comes from one line of a library. If an old API is called as jwt.decode(token, key) without specifying algorithms, the library accepts the header's alg as is and falls to confusion. That is why modern libraries made algorithms=["RS256"] a required argument. In this lab we experience why that one line is required by writing the verifier by hand.
kid injection looks like a normal verification even when you read the logs. "Verification passed, user admin" is printed, but in fact it is a token made with a key the attacker planted. What the verifier does with kid is revealed only by opening the code.
What you will do in the next lab
You make an RS256 key pair and an HS256 secret with cryptography and stand up your own JWT verifier at 8305. You check whether your own issued tokens pass both the manual verification and PyJWT, then make and throw alg:none, confusion, and wrong-kid tokens yourself and confirm they are rejected with a 401. You prove in one go keeping several keys in a JWKS and picking by kid, key rollover, and exp/nbf/iss/aud validation.