LabHub
开始
学习 学习路径 课程

会话与令牌 — 从浏览器到服务网格

JWT 签名验证与 alg 混淆、kid 注入防御

在 LabHub 中继续学习

한국어 원문으로 표시합니다.

목표

cryptography 로 만든 RS256 키쌍·HS256 비밀로 자기 JWT 검증기를 127.0.0.1:8305 에 세우고, alg:none·alg 혼동·kid 주입 위조 토큰을 직접 만들어 던져 방어가 상태코드로 막는지 확인한다. 손검증과 PyJWT 검증을 둘 다 본다.

왜 중요한가

JWT 는 토큰이 스스로 증거를 들고 다녀 서버에 물어보지 않고 서명만 검증하면 된다. 그 편리함의 대가로, 검증을 조금만 느슨하게 짜면 토큰 헤더의 alg 와 kid 를 검증기가 곧이곧대로 믿어 위조가 통과한다. alg:none 은 서명 없는 토큰을 허용하는 것이고, alg 혼동은 RS256 공개키를 HS256 비밀로 오용해 공개된 키만으로 서명을 위조하는 것이며, kid 주입은 검증에 쓸 키 자체를 공격자가 지정하는 것이다. RFC 8725(JWT BCP)의 방어는 셋으로 요약된다 — 허용 알고리즘 화이트리스트를 두고, 알고리즘을 헤더가 아니라 키 타입에 고정하고, kid 를 파일 경로나 URL 로 해석하지 않는다. 여기에 exp/nbf/iss/aud 검증을 더한다. 이 실습은 그 규칙들이 실제 위조 요청에서 지켜지는지를 스스로 증명하게 한다.

단계

  1. /root/st/jwt/app.py 를 127.0.0.1:8305 에 띄우고 자기 발급 RS256 토큰이 200 이며 PyJWT 로도 통과함을 /root/st/jwt/self.out 에 남긴다.
  2. alg:none 토큰을 만들어 던지면 401 인지 /root/st/jwt/none.out 에 적는다.
  3. rs-1 공개키를 HS256 비밀로 오용한 혼동 토큰은 401, 정당한 hs-1 HS256 토큰은 200 인지 /root/st/jwt/confusion.out 에 적는다.
  4. rs-1 토큰은 200, 헤더 kid 만 바꿔치기하면 401, 없는 kid 도 401 인지 /root/st/jwt/kid.out 에 적는다.
  5. JWKS 에 rs-1·rs-2 가 함께 있고 두 kid 토큰이 모두 200 인지 /root/st/jwt/rotation.out 에 적는다.
  6. kid 를 파일 경로로 둔 주입 토큰이 401 인지 /root/st/jwt/inject.out 에 적는다.
  7. exp/nbf/iss/aud 가 각각 어긋난 토큰이 401, 정상만 200 인지 /root/st/jwt/claims.out 에 적는다.
  8. /root/st/jwt/e2e.sh 로 valid·none·confusion·badkid 를 한 번에 던져 /root/st/jwt/e2e.out 에 네 줄을 남긴다.

참고

검증기 기동, 자기 발급 RS256 토큰 검증

/root/st/jwt/app.py 를 127.0.0.1:8305 에 띄운다. POST /sign 으로 받은 자기 발급 RS256 토큰을 POST /verify 에 던지면 200 이고, /root/st/jwt/verify_both.py 가 PyJWT 로도 통과함을 /root/st/jwt/self.outverify_code=200, pyjwt=valid ... 로 남긴다.

키는 서버가 켜질 때 cryptography 로 만들어 파일로 보존합니다. 서버는 백그라운드로 띄우고 준비될 때까지 폴링하세요. PyJWT 는 jwt.decode 에 algorithms=["RS256"] 을 반드시 넘겨야 합니다.

alg:none 토큰 거절

algnone 으로 두고 서명을 비운 토큰을 만들어 POST /verify 에 던지면 401 로 거절되는지 /root/st/jwt/none.outnone_code=401 로 적는다.

RFC 7519 §6 의 Unsecured JWS 입니다. 방어는 허용 alg 화이트리스트 하나로 끝납니다 — none 이 목록에 없으면 서명을 보기도 전에 걸립니다.

alg 혼동(RS 공개키를 HS 비밀로) 거절

rs-1 공개키(JWKS 의 n,e 로 복원한 SPKI PEM)를 HS256 비밀로 삼아 서명한 토큰은 401 로 거절되고, 정당한 hs-1 HS256 토큰은 200 으로 통과하는지 /root/st/jwt/confusion.outconfusion_code=401, legit_hs256_code=200 으로 적는다.

혼동을 막는 것은 HS256 을 금지하는 게 아니라 alg 를 키 타입에 고정하는 것입니다. rs-1 은 RSA 키라 RS256 전용이므로 헤더가 HS256 을 요구하면 서명을 보기 전에 거절됩니다.

kid 로 키 선택, 없는 kid 거절

rs-1 로 서명한 토큰은 200 이고, 그 토큰의 헤더 kid 만 rs-2 로 바꿔치기하면(서명은 그대로) 401, 없는 kid(rs-9)도 401 인지 /root/st/jwt/kid.outgood_kid_code=200, wrong_kid_code=401, unknown_kid_code=401 로 적는다.

kid 는 어느 키로 검증할지 고르는 손잡이입니다. 헤더 kid 만 바꾸면 실제 서명은 rs-1 키의 것이라 rs-2 로는 검증에 실패합니다.

JWKS 키 롤오버

JWKS 에 rs-1 과 rs-2 가 함께 있고(jwks_kids=rs-1,rs-2), 새 kid(rs-2)로 서명한 토큰도, 옛 kid(rs-1) 토큰도 모두 200 으로 통과하는지 /root/st/jwt/rotation.outjwks_kids=rs-1,rs-2, new_kid_code=200, old_kid_code=200 으로 적는다.

롤오버 중에는 옛 키로 서명된 토큰이 아직 돌아다닙니다. JWKS 에 두 키를 함께 두어 kid 로 골라 검증합니다 — 옛 키를 너무 일찍 지우면 유효한 토큰이 한꺼번에 거절됩니다.

kid 경로·URL 주입 거절

kid 를 파일 경로(/root/st/jwt/evil_pub.pem)로 두고 그 경로에 심은 키로 서명한 토큰을 던지면 401 로 거절되는지 /root/st/jwt/inject.outbadkid_code=401 로 적는다.

kid 는 아무 문자열이나 올 수 있습니다(RFC 7515 §4.1.4). 검증기가 kid 로 파일을 열거나 URL 을 당기면 공격자가 검증에 쓸 키를 지정하게 됩니다. kid 는 내가 아는 키 목록의 조회 키로만 쓰세요.

exp/nbf/iss/aud 검증

서명은 모두 정상이되 클레임만 어긋난 토큰들이 각각 401 이고 정상 토큰만 200 인지 /root/st/jwt/claims.outvalid_code=200, expired_code=401, notyet_code=401, badiss_code=401, badaud_code=401 로 적는다.

서명이 맞아도 끝이 아닙니다. exp 이 지났거나 nbf 가 미래거나 iss 가 내가 아는 발급자가 아니거나 aud 가 내 이름이 아니면 거절합니다. 시계 오차용 leeway 는 몇 분 이내로 둡니다.

종합: valid·none·confusion·badkid

/root/st/jwt/e2e.sh 로 네 가지를 한 번에 던져 /root/st/jwt/e2e.outvalid=200, none=401, confusion=401, badkid=401 을 남긴다.

앞 스텝들을 한 스크립트로 이어 붙입니다. 정상 토큰만 통과하고 세 위조는 모두 401 이어야 합니다.