WebアプリケーションでJWT(JSON Web Token:ジェイダブリューティー)を導入している現場のエンジニアの方、「JWTを使えば認証は安全」と思い込んでいませんか?実は、JWTには仕様上の落とし穴が複数あり、ライブラリの設定ひとつのミスがシステム全体の認証を突破されるリスクに直結することがあります。
この記事では、JWTに存在する主要な脆弱性と攻撃手法を防御の観点から解説します。alg:none攻撃・アルゴリズム混同攻撃・弱い秘密鍵の悪用など、実際に悪用された手口を理解した上で、安全な実装方法を具体的に説明します。フレームワークのJWT設定を見直したい方、社内システムのセキュリティレビューを担当している方にとって実践的な内容になっています。
JWTとは?なぜセキュリティ上の問題が起きるのか
JWT(JSON Web Token)は、ユーザー認証やAPI認可に広く使われるトークン形式の標準規格(RFC 7519)です。「サーバー側にセッション情報を保存しなくていい」「複数サービス間で認証情報を安全に受け渡せる」という利点から、マイクロサービスアーキテクチャやSPA(シングルページアプリケーション)で急速に普及しました。
JWTは3つのパーツで構成され、ピリオド(.)で区切られています。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJ1c2VySWQiOiIxMjMiLCJyb2xlIjoidXNlciJ9. SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
各パーツをBase64URLデコードすると中身が見えます。
・Header(ヘッダー): 使用するアルゴリズム(alg)とトークンタイプ(typ)を宣言します。例: {"alg":"HS256","typ":"JWT"}
・Payload(ペイロード): ユーザーIDやロール、有効期限など、アプリが必要とするクレーム(claim)を格納します。例: {"userId":"123","role":"user","exp":1716566400}
・Signature(シグネチャ): HeaderとPayloadをまとめて秘密鍵で署名した検証用データです。改ざん防止に使います
セキュリティ上の問題が起きる根本原因は「HeaderのアルゴリズムをサーバーがJWT自身から読み取る」設計にあります。攻撃者がHeaderを細工してアルゴリズムを書き換えると、サーバーが攻撃者の指定した方法で検証してしまう可能性があるのです。
また、JWTのHeaderとPayloadはBase64URLエンコードされているだけで暗号化はされていないため、トークンを入手した人は誰でも中身を読めます。これを正しく理解していないと、機密情報をPayloadに入れてしまうミスも起きます。
攻撃者が狙うJWTの主な弱点
1. alg:none攻撃 — 署名を完全に無効化する
JWT仕様には、署名アルゴリズムとして none が定義されています(RFC 7519 Section 6.1)。これは署名を使わない「非保護JWS」を意味しますが、古いライブラリや設定が不十分な実装では、このアルゴリズムをそのまま受け入れてしまうことがあります。
攻撃者はこの仕様を次のように悪用します。
# 正規のJWT Header(Base64URLデコード後) {"alg": "HS256", "typ": "JWT"} # 攻撃者が書き換えたHeader {"alg": "none", "typ": "JWT"} # 書き換えたHeaderをBase64URLエンコードして再組み立て # Signature部を空文字列にして送信する eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VySWQiOiIxIiwicm9sZSI6ImFkbWluIn0.
サーバー側がHeaderで指定されたアルゴリズムを信頼する実装になっていると、alg:noneの場合は「署名なしでOK」と判断します。Payloadのロールを admin に書き換えたトークンを送りつけるだけで、管理者権限を無条件に取得できてしまいます。
2015年以前の多くのJWTライブラリがこの問題を抱えていました。現在でも古いバージョンや設定ミスのある実装で悪用される可能性があります。
2. アルゴリズム混同攻撃(RS256→HS256)
RSA署名(RS256)を使うシステムでは、サーバーが公開鍵を検証に使います。公開鍵は名前のとおり公開されているため、攻撃者も入手できます。
この攻撃の流れはこうです。攻撃者はHeaderの alg を RS256 から HS256 に書き換えます。HS256は対称アルゴリズム(HMAC-SHA256)なので、署名と検証に同じ鍵を使います。ライブラリの実装によっては「HS256の検証鍵として何を使うか」をRSA用の設定からそのまま読み込み、RSA公開鍵をHS256の秘密鍵として扱ってしまうことがあります。
攻撃者はサーバーの公開鍵を入手し、それを秘密鍵として使ってHS256署名を作ります。サーバー側は「自分の公開鍵でHS256検証を行った結果が一致した」として、偽のトークンを有効と判定してしまいます。2015年に多数のJWTライブラリで発見された、実害の大きい攻撃パターンです。
3. 弱い秘密鍵によるオフラインブルートフォース
HS256などの対称アルゴリズムを使う場合、署名の安全性は秘密鍵の強度に完全に依存します。開発者が設定した短い秘密鍵(secret、password123、会社名など)は、オフラインの総当たりで解読されることがあります。
JWTのHeaderとPayloadは誰でも読めます。トークンを入手した攻撃者は、秘密鍵候補を次々と試して署名を検証する攻撃を行います。正しい鍵が見つかれば、任意のユーザーとして有効なトークンを自由に生成できます。よく使われるパスワードリストでも、単純な秘密鍵は数分で解読されてしまいます。
4. JKU/X5U Header Injection
JWT仕様には、署名検証に使う公開鍵セット(JWK Set)のURLを jku(JWK Set URL)ヘッダーで指定できる機能があります。本来は正規の鍵配布サーバーを参照するための機能です。
しかし、検証サーバーが jku の値を検証せずにリモートURLから鍵を取得する実装になっていると、攻撃者は自分が制御するURLを jku に指定できます。攻撃者は自分で生成した公開鍵セットをそのURLで提供し、対応する秘密鍵で署名したトークンを送ります。サーバーは攻撃者のURLから鍵を取得して検証するため、偽のトークンが有効と判定されます。
5. 重要クレームの未検証
署名の正当性確認だけに集中し、Payloadのクレーム検証が不十分なケースも多く見られます。
・exp(有効期限)の未検証: 期限切れのトークンが永続的に使い続けられる恐れがあります
・nbf(Not Before)の未検証: まだ有効期間が始まっていないトークンを受け入れてしまいます
・iss(発行者)の未検証: 別のサービスが発行したトークンをそのまま受け入れてしまいます
・aud(対象者)の未検証: 別のAPIエンドポイント向けに発行されたトークンが流用されます
・sub(主体)の実在確認漏れ: 削除済みのユーザーIDのトークンでアクセスを許可してしまいます
具体的な防御手順
1. アルゴリズムをサーバー側で固定する(最重要)
JWTを検証する際、Headerで指定されたアルゴリズムを信頼してはいけません。サーバー側でアルゴリズムを明示的に指定し、それ以外を完全に拒否します。これだけでalg:none攻撃とアルゴリズム混同攻撃の大部分を防ぐことができます。
# Python(PyJWT)での実装例 # NGパターン(algをトークン側から読む) decoded = jwt.decode(token, public_key) # OKパターン(algをサーバー側で明示) decoded = jwt.decode( token, public_key, algorithms=["RS256"] # noneや他のalgは絶対に含めない )
# Node.js(jsonwebtoken)での実装例 # NGパターン(アルゴリズムを指定しない) jwt.verify(token, publicKey); # OKパターン(アルゴリズムを明示指定) jwt.verify(token, publicKey, { algorithms: ['RS256'] });
2. 対称アルゴリズム使用時の秘密鍵強度を確保する
HS256/HS384/HS512を使う場合、使用するアルゴリズムの出力長以上のランダムな秘密鍵が必要です。HS256なら256ビット(32バイト)以上、HS512なら512ビット(64バイト)以上が推奨されています。
# 十分な強度の秘密鍵を生成する(Linux) openssl rand -hex 32 # 出力例: 4a8f2e3b1c9d6e7f5a0b3c4d8e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f # Pythonで生成する場合 python3 -c "import secrets; print(secrets.token_hex(32))"
生成した秘密鍵は環境変数または専用のシークレット管理ツール(AWS Secrets Manager、HashiCorp Vault、GCP Secret Managerなど)で管理します。アプリケーションコードやGitリポジトリには絶対に含めないようにします。
3. すべての重要クレームを検証する
署名の検証と同時に、以下のクレームを必ず検証します。多くのライブラリでは検証オプションを有効化するだけで対応できます。
| クレーム | 検証内容 | 未検証の場合のリスク |
|---|---|---|
| exp | 現在時刻がexpより前か | 期限切れトークンが無期限で有効になる |
| nbf | 現在時刻がnbf以降か | 有効期間前のトークンが使われる |
| iss | 信頼する発行者と一致するか | 別システムのトークンで認証が通る |
| aud | 自サービスを対象にしているか | 他サービス向けトークンが流用される |
| sub | ユーザーIDが実在するか | 削除済みアカウントでアクセスされる |
4. 非対称アルゴリズム(RS256/ES256)の採用を優先する
マイクロサービスなど複数のサービスがJWTを検証する環境では、対称アルゴリズム(HS256)よりもRSA(RS256)やECDSA(ES256)などの非対称アルゴリズムを推奨します。非対称アルゴリズムでは署名に使う秘密鍵と検証に使う公開鍵が分離されているため、仮に検証サーバーが侵害されても署名の偽造はできません。
5. 信頼できるライブラリを最新版で使用する
JWTの署名・検証を自前実装しようとするのは避けてください。ライブラリにはセキュリティパッチが継続的にリリースされています。使用中のライブラリのCVEを定期確認し、常に最新版を使うことが重要です。
# npm auditでJavaScriptの依存ライブラリを確認 npm audit # Pythonパッケージの脆弱性チェック pip install pip-audit pip-audit
6. jku/x5uの検証を厳格にする
jku や x5u を使用する場合、取得先URLをホワイトリストで厳格に制限します。信頼できる鍵配布サーバーのURLのみを許可し、任意のURLを受け入れない実装にします。可能であれば、これらのヘッダーを使用しない設計を選択することも有効な選択肢です。
中小企業でも今日からできること
JWTを直接実装することは少なくても、LaravelやSpring Boot、FastAPIなどのフレームワーク、あるいはFirebase AuthenticationやAuth0といった認証サービスを使う場合でも、JWT関連の設定ミスは起こり得ます。今日から実践できる対策を整理しました。
・使用フレームワークのJWT設定を確認する: Laravel Passport/Sanctum、Spring Securityなど、各フレームワークのJWT設定でアルゴリズムが明示指定されているか確認します
・alg:noneが無効か確認する: ライブラリのドキュメントやGitHub issueで none アルゴリズムが拒否されているか確認します
・秘密鍵を環境変数で管理する: .env ファイルで管理し、Gitの .gitignore に追加して絶対にリポジトリに含めないようにします
・npm audit / pip-auditをCIに組み込む: プルリクエストのたびに自動実行することで、脆弱なライブラリをリリース前に検出できます
・JWT関連の署名検証エラーをログで監視する: 不正なアルゴリズム指定や署名検証失敗のログが急増している場合、攻撃試行の可能性があります。SIEMやログ監視ツールでアラートを設定しましょう
JWTに関するよくある質問(FAQ)
Q. JWTとセッションCookieはどちらが安全ですか?
一概にどちらが安全とは言えません。セッションCookieはサーバー側で状態を管理するため、即座にセッションを無効化できる反面、セッションストレージへの負荷があります。JWTはステートレスでスケーラブルですが、即時失効が難しいという制約があります。セキュリティ要件と運用規模を総合的に判断して選択します。
Q. トークンの有効期限はどのくらいに設定すべきですか?
一般的なWebアプリでは、アクセストークンを短め(15分~1時間)に設定し、リフレッシュトークンと組み合わせる設計が推奨されています。短い有効期限にすることで、トークンが漏洩した場合の被害時間を最小化できます。決済・個人情報変更などセンシティブな操作では、さらに短い有効期限か再認証を要求する設計が望ましいです。
Q. JWTをLocalStorageに保存するのは問題ありますか?
LocalStorageに保存されたJWTはXSS(クロスサイトスクリプティング)攻撃で盗まれる可能性があります。セキュリティ上は HttpOnly フラグ付きのCookieに保存することが推奨されています。ただし、Cookieに保存した場合はCSRF対策が別途必要になります。アプリの要件に応じてトレードオフを検討してください。
よくある誤解と注意点
【誤解1】「JWTはBase64エンコードされているから内容が読めない」
JWTのHeaderとPayloadはBase64URLエンコードされているだけで、暗号化はされていません。ブラウザのコンソールやオンラインデコーダー(jwt.ioなど)で誰でも即座に中身を読めます。機密度の高い情報をPayloadに含める場合は、JWE(JSON Web Encryption)による暗号化を別途検討してください。
【誤解2】「署名が正しければそれだけで安全」
署名の検証は「トークンが改ざんされていないこと」を証明するだけです。「発行元が正当か」「有効期限内か」「このサービス向けか」は署名の検証では分かりません。クレーム検証(exp・iss・aud)を組み合わせることで初めてトークン認証として機能します。
【誤解3】「トークンを再発行すれば古いトークンは無効になる」
JWTはステートレスが設計上の特徴です。サーバー側に状態を持たないため、発行済みのJWTを即座に無効化(revoke)する手段が原則ありません。退職者のアカウント即時停止や、不正アクセス検知後のセッション強制終了が必要な場合は、トークンの失効リストをDBやRedisで管理する仕組みを別途実装する必要があります。
【注意】JWTライブラリのセキュリティアドバイザリを定期確認する
JWTライブラリは過去に複数の重大な脆弱性が発見されています。GitHubの「Security advisories」タブや、使用言語のパッケージマネージャーのセキュリティアラートを定期的に確認する習慣をつけましょう。特に、長期間バージョンアップしていないプロジェクトは注意が必要です。
本記事のまとめ
| 攻撃手法 | 主な原因 | 優先対策 |
|---|---|---|
| alg:none攻撃 | サーバーがalgをトークンから読む | algをサーバー側で明示固定 |
| アルゴリズム混同攻撃 | RS256とHS256の混同実装 | アルゴリズム明示指定・非対称推奨 |
| 弱い鍵のブルートフォース | 短・予測可能な秘密鍵 | 256ビット以上のランダム鍵を生成 |
| JKU/X5U Injection | 外部URL鍵セットの無条件信頼 | 取得先URLをホワイトリストで制限 |
| クレーム未検証 | exp・iss・audの検証省略 | ライブラリの検証オプションを全有効化 |
JWTは正しく実装すれば強力な認証メカニズムになりますが、「仕様として存在する落とし穴」が多い技術です。最大の防御は「アルゴリズムをサーバー側で固定すること」と「すべてのクレームを検証すること」の2点です。自前実装は避け、信頼できるライブラリを最新バージョンで使い続け、定期的にセキュリティアドバイザリを確認する習慣が重要です。
Linuxサーバー上でのファイル権限・sudo設定など、認証周辺のLinuxセキュリティについては、姉妹サイトLinuxMaster.JPでも詳しく解説しています。
JWTの設定、本当に安全か確認できていますか?
「署名検証しているから大丈夫」と思っていたら、alg:none攻撃が通る設定だった――そんな事故を防ぐための実践的なセキュリティ知識を体系的に身につけませんか。
正しいセキュリティ知識を体系的に身につけたい方へ、メルマガで実践的なセキュリティ対策ノウハウをお届けしています。
