JSON Web Tokenセキュリティ:一般的な脆弱性と対策
JWTはAPI認証のデファクトスタンダードですが、その一見シンプルな見た目の裏には実際のセキュリティの落とし穴が潜んでいます。誤った設定のJWTは、認証バイパス、権限昇格、データ漏洩につながってきました。このガイドでは、最も重大な脆弱性とその防止方法を解説します。
JWT構造のおさらい
JWTはドットで区切られた3つのBase64URLエンコードされた部分で構成されます:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ.signature
- ヘッダー:アルゴリズムとトークンタイプ
- ペイロード:クレーム(ユーザーデータ、有効期限など)
- 署名:暗号学的検証
JWTの構造はJWTエンコーダー/デコーダーで確認できます。
JWT構造の基礎的な理解については、JWTトークン解説ガイドをご覧ください。
重大な脆弱性
1. アルゴリズム混同攻撃
最も危険なJWT脆弱性です。サーバーがトークンのalgヘッダーを検証なしに受け入れる場合、攻撃者は以下のことが可能になります:
攻撃:アルゴリズムをRS256(非対称)からHS256(対称)に変更し、偽造トークンをサーバーの公開鍵で署名:
// Attacker's forged token
header: { "alg": "HS256", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
// Signed with the server's PUBLIC key as the HMAC secret
サーバーが公開鍵をシークレットとしてHS256トークンを検証する場合、偽造トークンが検証を通過します。
対策:期待されるアルゴリズムを常に明示的に指定:
// WRONG - accepts whatever algorithm the token specifies
jwt.verify(token, key);
// CORRECT - enforce specific algorithm
jwt.verify(token, key, { algorithms: ['RS256'] });
2. Noneアルゴリズム攻撃
一部のライブラリは"alg": "none"を受け入れます — 署名のないトークンです:
// Forged token with no signature
header: { "alg": "none", "typ": "JWT" }
payload: { "sub": "admin", "role": "superadmin" }
signature: "" // empty
対策:本番環境でnoneアルゴリズムを絶対に許可しない。許可するアルゴリズムを明示的にホワイトリスト化。
3. 弱い署名シークレット
HMACベースのJWT(HS256/HS384/HS512)はシークレットの強度に依存します:
// TERRIBLE - can be brute-forced in seconds
secret = "password123"
// WEAK - dictionary attack vulnerable
secret = "my-jwt-secret"
// STRONG - 256+ bits of randomness
secret = "a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0"
対策:HMACシークレットには少なくとも256ビットの暗号学的ランダム性を使用。より良い方法として、署名鍵を共有する必要のない非対称鍵(RS256、ES256)を使用。
4. 有効期限の欠如
有効期限のないトークンは永久に有効です — 盗まれたトークンが永続的なアクセスを与えます:
// WRONG - no expiration
{ "sub": "user123", "role": "admin" }
// CORRECT - short expiration
{
"sub": "user123",
"role": "admin",
"exp": 1705312800,
"iat": 1705309200,
"nbf": 1705309200
}
ベストプラクティス:アクセストークンは15〜60分で有効期限切れ。長いセッションにはリフレッシュトークン(安全に保存)を使用。
5. ペイロード内の機密データ
JWTペイロードはBase64URLエンコードされているだけで、暗号化されていません。誰でもデコードできます:
echo "eyJzdWIiOiIxMjMiLCJyb2xlIjoiYWRtaW4ifQ" | base64 -d
# {"sub":"123","role":"admin"}
絶対に含めてはいけないもの:パスワード、APIキー、クレジットカード番号、個人データ(社会保障番号、医療記録)、その他のシークレット。
6. トークン保存(XSS vs CSRF)
| 保存場所 | XSS脆弱 | CSRF脆弱 |
|---|---|---|
| localStorage | はい | いいえ |
| Cookie(フラグなし) | はい | はい |
| HttpOnly Cookie | いいえ | はい |
| HttpOnly + SameSite Cookie | いいえ | いいえ |
推奨:JWTをHttpOnly、Secure、SameSite=Strict Cookieに保存。これによりJavaScriptアクセス(XSS防御)とクロスサイトリクエスト(CSRF防御)を防止します。
Set-Cookie: token=eyJ...; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=3600
セキュリティチェックリスト
- アルゴリズムを明示的に指定 — トークンヘッダーからの指定を受け入れない
noneアルゴリズムを拒否 — 常に署名を要求- 強力なシークレットを使用 — HMACには256ビット以上、RSAには2048ビット以上
- 短い有効期限を設定 — アクセストークンは15〜60分
- すべてのクレームを検証 —
exp、iss、aud、nbf - HttpOnly Cookieに保存 — localStorageではなく
- シークレットを定期的にローテーション — 鍵のローテーションを計画
- ペイロードに機密データを保存しない
- 分散システムには非対称鍵を使用(RS256またはES256)
- トークン失効を実装 — ブラックリストまたは短い有効期限 + リフレッシュトークン
非対称署名 vs 対称署名
| 側面 | HS256(対称) | RS256(非対称) |
|---|---|---|
| 鍵 | 共有シークレット | 秘密鍵/公開鍵ペア |
| 署名できる人 | シークレットを持つ全員 | 秘密鍵の保持者のみ |
| 検証できる人 | シークレットを持つ全員 | 公開鍵を持つ全員 |
| 鍵の配布 | シークレットを安全に共有する必要あり | 公開鍵のみ共有 |
| パフォーマンス | 高速 | 低速 |
| 最適な用途 | 単一サービス | マイクロサービス、分散システム |
推奨:新規アプリケーションにはES256(ECDSA)を使用。HMACに近いパフォーマンスで非対称セキュリティを提供します。
トークン失効戦略
JWTは設計上ステートレスです — 組み込みの失効方法はありません。戦略:
- 短い有効期限:トークンが15分で期限切れなら、盗まれたトークンの有効期間は限定的
- リフレッシュトークンローテーション:使用ごとに新しいリフレッシュトークンを発行。リフレッシュトークンが2回使用されたら、すべてのトークンを失効
- ブラックリスト:失効したトークンID(jtiクレーム)を保存し、各リクエストで確認
- トークンバージョニング:クレームにバージョン番号を含め、ログアウト時にユーザーのバージョンをインクリメント
FAQ
認証にはJWTとセッションCookieのどちらを使うべきですか?
セッションCookieは従来のWebアプリケーションにはよりシンプルで安全です。サーバーがセッションライフサイクルを制御し、失効は即座です。JWTはステートレスAPI、マイクロサービス、サーバー側のセッション保存が現実的でないモバイルアプリに適しています。JWTを選択する場合は、このガイドのセキュリティ対策を実装してください。
JWTの理想的な有効期限は?
アクセストークン:15〜60分。短いほど安全ですが、より頻繁な更新が必要です。リフレッシュトークン:1〜30日、HttpOnly Cookieに保存。単発使用トークン(メール確認、パスワードリセット):1〜24時間。
関連リソース
- JWTエンコーダー/デコーダー — JWTを安全に検査・デコード
- JWTトークン解説 — JWT構造とフローを理解
- パスワードエントロピー解説 — JWT署名シークレットの強度