1. JWT の構造 #structure
JWT は3つの base64url エンコード部分をドット . で連結した文字列。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoidXNlciJ9.{signature}
^ ^ ^
ヘッダ(JSON) ペイロード(JSON) 署名
- ヘッダ — 署名アルゴリズム(
alg)とトークンタイプ。例:{"alg":"HS256","typ":"JWT"} - ペイロード — クレーム(ユーザ ID、権限、有効期限等)。例:
{"sub":"123","role":"user","exp":1720000000} - 署名 — ヘッダ + ペイロードに対するアルゴリズムベースの署名。改ざん検知用
ヘッダとペイロードは暗号化されておらず、誰でも base64 デコードして読める(機密情報を入れてはいけない)。
2. alg=none 攻撃 #alg-none
JWT 仕様には alg: "none"(署名なし)モードが定義されています。これを「デバッグ用」「テスト用」と思って実装に残したサーバが攻撃される。
// 攻撃者が改ざん:
ヘッダ: {"alg":"none","typ":"JWT"}
ペイロード: {"sub":"123","role":"admin"}
署名: (空文字列)
サーバが alg ヘッダの値をそのまま信頼して「none なら署名検証スキップ」と実装していると、攻撃者は何でも書き換えられる。
3. alg confusion(HS256 / RS256 取り違え) #alg-confusion
典型的な事例。サーバが RS256(公開鍵暗号、署名は秘密鍵で、検証は公開鍵で)で運用しているとする。攻撃者は alg を HS256(共通鍵暗号、HMAC)に書き換え、公開鍵をそのまま HMAC の鍵として使って署名する。
- サーバの公開鍵を入手(
/.well-known/jwks.json等で公開されている) - JWT の alg を RS256 → HS256 に書き換え、ペイロードを改ざん
- 公開鍵を HMAC 鍵として、改ざんしたヘッダ+ペイロードに対する HS256 署名を計算
- サーバ側のコードが
jwt.verify(token, public_key)と書かれていて、alg を JWT 内の値に従ってしまうと、HS256 として公開鍵を HMAC 鍵に使って検証 → 一致 → トークンが正当と判定される
これは仕様の柔軟性とライブラリのデフォルトが噛み合った結果。「期待する alg をサーバ側で明示的に指定」することで防げる。
4. 弱い HMAC 鍵のブルートフォース #weak-secret
HS256 系で短い・予測可能な鍵(secret、password、社名等)を使っていると、JWT 自体は公開情報なので hashcat 等でオフライン総当たり可能。
$ hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
HMAC 鍵には 256bit 以上の暗号論的にランダムな値を使う。Argon2/bcrypt とは別物で、こちらは「速く検証する必要があるので低速ハッシュは使えない」が、鍵のエントロピーは確保する必要がある。
5. 正しい防御 #defense
- 検証時に期待する alg を明示 —
jwt.verify(token, key, { algorithms: ['RS256'] })と書く。JWT 内の alg ヘッダは信用しない - none アルゴリズムは禁止 — ライブラリ・設定で alg=none を絶対に許可しない
- 鍵の管理 — HMAC 鍵は CSPRNG で生成した 256bit 以上。秘密鍵は HSM や KMS に
- ペイロードに機密情報を入れない — base64 デコードで誰でも読める。ID 程度に留める
- 有効期限を短く —
expを短く(15分等)。漏洩時の被害時間を限定。リフレッシュトークンと併用 - サーバ側で失効リストを持つ — JWT は本来「ステートレス」だが、ログアウト・侵害発覚時の即時無効化のため、失効リスト(またはセッション ID 連動)が現実的
- セッション Cookie で十分なら使う — マイクロサービス間の認証等で必要なケース以外、伝統的なサーバセッションの方が安全な選択肢