JWT のセキュリティベストプラクティス|alg none 攻撃 / 有効期限 / 署名検証
JWT (JSON Web Token) はモダンな Web API の認証でデファクトスタンダードですが、<strong>仕様を誤解したまま実装すると簡単に脆弱性を生みます</strong>。本記事では実際に多く見られる JWT の誤用パターンと、それを避けるためのベストプラクティスを解説します。
JWT の構造おさらい
JWT は 3 つの Base64URL エンコード文字列をドットで連結した形式です。
eyJhbGci... . eyJzdWIi... . SflKxwRJSM...
header payload signature
- <strong>header</strong>: 署名アルゴリズム (<code>alg</code>) とトークン種別 (<code>typ</code>)
- <strong>payload</strong>: クレーム (sub / iss / aud / exp / iat / ...) の JSON オブジェクト
- <strong>signature</strong>: header と payload を対象にした HMAC / RSA / ECDSA 署名
JWT の中身は <a href="/ja/tools/jwt/">DevLab の JWT デコーダ</a> で確認できます。Base64URL はエンコードであって <strong>暗号化ではない</strong> ので、payload の中身はサーバで署名検証しなくても誰でも読めることを忘れずに。
脅威 1: alg=none 攻撃
JWT 仕様には <code>"alg": "none"</code> という「署名なし」のアルゴリズム指定が存在します。攻撃者がこれを利用して <code>{"alg":"none"}</code> のヘッダで payload を書き換え、ライブラリがそのまま受け入れると <strong>任意のユーザーに成りすませる</strong> 致命的な脆弱性になります。
<strong>対策:</strong>
- 検証時に許可するアルゴリズムを <strong>明示的にホワイトリスト</strong> 指定する
- <code>jwt.verify(token, secret, { algorithms: ['HS256'] })</code> のように配列で渡す
- 2015 年頃のライブラリは脆弱だったが現在の主要ライブラリは対策済み。ただし自作しない限り最新版を使うこと
// ✗ 悪い例 (アルゴリズム未指定 = ライブラリが alg ヘッダを信頼)
jwt.verify(token, secret);
// ✓ 良い例 (アルゴリズムを固定)
jwt.verify(token, secret, { algorithms: ['HS256'] });
脅威 2: 鍵混同攻撃 (key confusion)
RSA 公開鍵を持っている攻撃者が、<strong>アルゴリズムを RS256 から HS256 に変える</strong> ことで公開鍵を「秘密鍵」として扱わせる攻撃です。JWT ライブラリが <code>alg</code> を信頼して検証関数を選ぶと、公開鍵で HMAC 署名された偽トークンが通ってしまいます。
<strong>対策:</strong> アルゴリズムは必ず検証側で固定する (脅威 1 と同じ対策)。さらに、鍵の種類も明示的に区別する:
- HS256 なら <code>Buffer</code> で渡す
- RS256 なら PEM フォーマットの公開鍵で渡す
脅威 3: 有効期限の未検証 / 無期限トークン
JWT の <code>exp</code> クレームは有効期限を示す UNIX 秒ですが、<strong>検証時に無視されているケース</strong> がよくあります。一度発行されたトークンが永久に使えると、漏洩時に被害が止まりません。
<strong>対策:</strong>
- 発行時に <code>exp</code> を短く設定 (アクセストークンは 15 分〜1 時間が目安)
- 検証時に必ず <code>exp</code> をチェック (主要ライブラリは自動でやる)
- 長期セッションが必要ならリフレッシュトークンパターンを使う (アクセストークン短命 + リフレッシュトークン長命 + サーバサイド失効リスト)
脅威 4: JWT に機密情報を入れる
JWT の payload はただの Base64 エンコードで、誰でも復号できます。それを知らずに <strong>パスワードやクレジットカード番号を payload に入れる</strong> 実装が後を絶ちません。
<strong>対策:</strong>
- payload には「このユーザーである」という最小限の識別情報だけ入れる (sub / user_id / role)
- 機密情報はサーバサイドのデータベースに置き、JWT からは user_id で引く
- どうしても機密情報を JWT で送る必要がある場合は <strong>JWE</strong> (暗号化版 JWT) を使う
脅威 5: 失効 (logout) ができない
JWT はステートレスで発行すると取り消せません。ユーザーがログアウトしても、サーバ側にトークン情報がないため「有効期限まで使えてしまう」。パスワード変更しても発行済みの JWT は生き続けます。
<strong>対策:</strong>
- 短い <code>exp</code> (15 分以下) で被害ウィンドウを最小化
- ログアウト時はサーバ側の <strong>失効リスト (denylist)</strong> に <code>jti</code> (JWT ID) を登録して、検証時に照合
- 重大なイベント (パスワード変更・権限変更) 時は <code>token_version</code> をユーザーレコードに保存して、検証時に一致を確認
脅威 6: 弱いシークレット
HS256 のシークレットが短すぎると、総当たりで解読されます。特に <code>"secret"</code> <code>"password123"</code> といった文字列は即死です。
<strong>対策:</strong>
- HS256 は少なくとも <strong>256 ビット (32 バイト)</strong> のランダム値を使う
- 生成は <code>openssl rand -base64 32</code> や <a href="/ja/tools/password/">パスワード生成ツール</a> で
- シークレットは絶対に Git にコミットしない。環境変数・Secrets Manager で管理
ベストプラクティスのまとめ
- 検証側でアルゴリズムを固定 (<code>algorithms: ['HS256']</code>)
- HS256 のシークレットは 256 ビット以上、RS256 なら 2048 ビット以上の RSA
- アクセストークンの <code>exp</code> は 15 分〜1 時間
- リフレッシュトークンパターンで長期セッションを実現
- payload には機密情報を入れない (Base64 は暗号化ではない)
- ログアウト時は失効リストに <code>jti</code> を登録
- シークレットは環境変数・Secrets Manager で管理、コミット厳禁
- クライアント側では JWT を localStorage ではなく HttpOnly Cookie に格納 (XSS 対策)
デバッグに役立つツール
- <a href="/ja/tools/jwt/">JWT デコーダ</a>: header / payload / signature を可視化、claim ごとの意味を解説付き表示
- <a href="/ja/tools/jwt-sign/">JWT 生成・署名ツール</a>: HS256 / HS384 / HS512 で Web Crypto API を使ってブラウザ内で署名。テスト用トークンの生成に便利
- <a href="/ja/tools/password/">パスワード生成ツール</a>: シークレットの生成に使えます
まとめ
JWT は正しく使えば便利な認証トークンですが、仕様の罠と攻撃パターンを知らないと本番システムに脆弱性を仕込むことになります。本記事で示した 6 つの脅威と対策を押さえ、JWT ライブラリの最新版を使うことが基本です。定期的に実装を見直し、セキュリティアドバイザリを追うことをおすすめします。