TT Lab
はじめる
学ぶ 学習パス コース

セッションとトークン — ブラウザからメッシュまで

JWT 署名検証と alg 混同・kid 注入への防御

TT Labで続きを見る

目標

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)の防御は3つにまとめられます。許可するアルゴリズムを許可リストで限定すること、アルゴリズムをヘッダーではなく鍵の種類に固定すること、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が一緒にあり、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に4行を残してください。

参考

検証器を起動し、自分で発行した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.outにverify_code=200、pyjwt=valid ...として残してください。

鍵はサーバーが起動するときにcryptographyで作ってファイルに保存します。サーバーはバックグラウンドで起動し、準備ができるまでポーリングしてください。PyJWTはjwt.decodeにalgorithms=["RS256"]を必ず渡す必要があります。

alg:noneトークンを拒否する

algをnoneにして署名を空にしたトークンを作り、POST /verifyに投げると401で拒否されるかを/root/st/jwt/none.outにnone_code=401と書いてください。

RFC 7519 §6のUnsecured JWSです。防御は許可するalgの許可リスト1つで済みます。noneが一覧になければ、署名を見る前に防げます。

alg混同(RSの公開鍵をHSの秘密鍵に)を拒否する

rs-1の公開鍵(JWKSのn,eから復元したSPKI PEM)をHS256の秘密鍵として署名したトークンは401で拒否され、正当なhs-1のHS256トークンは200で通るかを/root/st/jwt/confusion.outにconfusion_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.outにgood_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.outにjwks_kids=rs-1,rs-2、new_kid_code=200、old_kid_code=200と書いてください。

ロールオーバーの間は、古い鍵で署名されたトークンがまだ出回っています。JWKSに2つの鍵を一緒に置き、kidで選んで検証します。古い鍵を早く消しすぎると、有効なトークンが一度に拒否されます。

kidのパス・URL注入を拒否する

kidをファイルパス(/root/st/jwt/evil_pub.pem)にして、そのパスに仕込んだ鍵で署名したトークンを投げると401で拒否されるかを/root/st/jwt/inject.outにbadkid_code=401と書いてください。

kidにはどんな文字列でも来られます(RFC 7515 §4.1.4)。検証器がkidでファイルを開いたりURLを取得したりすると、攻撃者に検証に使う鍵を指定されます。kidは、自分が知っている鍵の一覧の検索キーとしてだけ使ってください。

exp/nbf/iss/audを検証する

署名はすべて正常で、クレームだけがずれたトークンがそれぞれ401になり、正常なトークンだけが200になるかを/root/st/jwt/claims.outにvalid_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で4種類を一度に投げ、/root/st/jwt/e2e.outにvalid=200、none=401、confusion=401、badkid=401を残してください。

前のステップを1つのスクリプトにつなげます。正常なトークンだけが通り、3つの偽造はすべて401になる必要があります。