自社のWebアプリやAPIにJWT認証を採用しているエンジニアの方から、「JWTって本当に安全なの?」という質問を受けることが多くなりました。JWT自体は優れた設計ですが、実装の仕方を誤ると深刻な認証回避につながります。
この記事では、JWT脆弱性の代表的な攻撃手口を「防御のために知る」という視点で解説します。署名検証のバイパス手法やアルゴリズム混乱攻撃の仕組みを理解し、自分のアプリに同じ穴が開いていないかを確認するための実践的な内容です。

JWT(JSON Web Token)とは?
JWTは、ユーザーの認証情報やセッション情報をJSON形式でエンコードし、署名を付けてURL安全な文字列として扱う仕様です(RFC 7519)。ヘッダー・ペイロード・署名の3つをBase64URLエンコードしてドット(.)でつないだ構造で、Webアプリ・モバイルアプリ・マイクロサービスのAPI認証で広く使われています。
# JWTの構造(例) # ヘッダー(Base64URLデコード後) {"alg": "HS256", "typ": "JWT"} # ペイロード(Base64URLデコード後) {"sub": "user123", "role": "user", "exp": 1757000000} # 完成形 (3パートをドットで連結) eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIi4...<署名>
JWTの安全性は「署名の検証」によって保たれています。サーバーは受け取ったJWTの署名を秘密鍵で検証し、ペイロードが改ざんされていないことを確認します。この検証が正しく機能しない場合に、脆弱性が生まれます。
JWT脆弱性の種類と攻撃の仕組み
1. algフィールドに「none」を指定する攻撃
JWTのヘッダーにある alg フィールドは署名アルゴリズムを指定しますが、一部のJWTライブラリは "alg": "none" を受け付けます。これは「署名なし」を意味し、攻撃者がこの値を指定すると署名部分を空にしたJWTを作り、検証をスキップさせることができます。
攻撃の流れ:
・攻撃者が正規のJWTを取得
・ヘッダーを {"alg": "none", "typ": "JWT"} に書き換える
・ペイロードの "role": "user" を "role": "admin" に書き換える
・署名部分を空にして送信
・脆弱なサーバーが「署名なし」を受け入れて管理者として処理
2. RS256→HS256 アルゴリズム混乱攻撃
これは多くのセキュリティエンジニアが気づきにくい巧妙な攻撃です。本来RS256(RSA署名)を使うサーバーに対して、攻撃者がHS256(HMAC)で署名したJWTを送りつけます。
RS256では「秘密鍵で署名し、公開鍵で検証」します。攻撃者は公開鍵を入手(多くのAPIは公開鍵を公開している)し、その公開鍵をHS256の「共有秘密鍵」として使ってJWTに署名します。脆弱なライブラリがアルゴリズムをクライアント任せで切り替えると、サーバーが「公開鍵をHS256の鍵として使って検証」してしまい、攻撃者が作ったJWTを正規と判定します。
# 攻撃者の視点(概念的なフロー) # 1. サーバーの公開鍵(RS256用)を取得 # 2. HS256アルゴリズムで、取得した公開鍵を「秘密鍵」として使用 # 3. ペイロードに昇格後の権限情報を入れてJWTを生成・送信 # 4. 脆弱なサーバーが公開鍵でHS256検証を行い、正当と判断してしまう
3. 弱いシークレットキーへのブルートフォース
HS256を使う場合、署名の強度はシークレットキーの複雑さに依存します。短いキー(8文字など)や辞書に載るような文字列(secret、password)を使っていると、ツールで総当たり攻撃をかけられシークレットキーが特定されます。鍵が判明すれば、攻撃者は任意のJWTを自分で正しく署名して送り込むことができます。
4. JWKSエンドポイントのURLインジェクション(CVE-2022-21449系の亜種)
JWTヘッダーには jku(JWK Set URL)や x5u(X.509証明書URL)などのフィールドがあり、検証に使う公開鍵の場所をクライアント側から指定できる場合があります。攻撃者が自分のサーバーのURLを jku に書き込み、そこに攻撃者が生成した公開鍵を置くと、サーバーが攻撃者の公開鍵を使って検証してしまいます。これにより、攻撃者が対応する秘密鍵で署名したJWTが正規と判断されます。
5. ペイロードの直接読み取りによる情報漏洩
JWTのヘッダーとペイロードはBase64URLエンコードされているだけで、暗号化はされていません(JWE=JSON Web Encryptionを使わない限り)。したがって、JWTを取得した第三者は署名の検証なしにペイロードの内容を読めます。機密情報(個人情報・内部ロール名・シークレットのヒントになる値)をペイロードに含めると情報漏洩につながります。
具体的な防御手順
1. algを必ずサーバー側で固定する
「クライアントが指定したアルゴリズムを使う」設計は絶対に避けます。サーバー実装でアルゴリズムを明示的に指定し、それ以外のアルゴリズムを持つJWTは即座にリジェクトします。
# Python: PyJWT での正しい検証(アルゴリズムを明示) import jwt # NG: algorithmsを指定しない(クライアント任せになる) # payload = jwt.decode(token, public_key, options={"verify_signature": True}) # OK: 使用するアルゴリズムをサーバー側で固定する payload = jwt.decode( token, public_key, algorithms=["RS256"] # RS256 のみ受け付ける ) # "none" アルゴリズムやHS256への切り替えは自動的に拒否される
2. シークレットキーを十分に長くランダムにする
HS256を使う場合、シークレットキーは最低256ビット(32バイト)以上のランダム値を使います。人間が覚えられるような文字列は使いません。
# 安全なシークレットキーの生成(Linux) # 32バイト(256ビット)の乱数をHex文字列で生成 openssl rand -hex 32 # 環境変数に格納し、コードにハードコードしない # export JWT_SECRET="生成した値"
3. 公開鍵の利用場所を固定する(jku/x5uを使わない)
JWTヘッダーの jku・x5u・kid フィールドを動的に参照する設定は、原則として使用しません。使う場合はホワイトリストによるURL検証を必ず実施します。
# Node.js: jose ライブラリでのホワイトリスト検証の概念 // NG: 任意のjkuを信頼する // const { payload } = await jwtVerify(token, createRemoteJWKSet(new URL(header.jku))); // OK: 自分のサーバーのJWKSエンドポイントのみ固定で指定する const TRUSTED_JWKS_URL = 'https://your-auth-server.example.com/.well-known/jwks.json'; const JWKS = createRemoteJWKSet(new URL(TRUSTED_JWKS_URL)); const { payload } = await jwtVerify(token, JWKS, { algorithms: ['RS256'] });
4. 有効期限(exp)と発行時刻(iat)を必ず検証する
JWTの有効期限を設定しないと、漏洩したトークンが永久に使い回されます。exp クレームを必ず含め、ライブラリの検証オプションで期限切れチェックを有効にします。
# JWTペイロードに必須のクレームを含める例 { "sub": "user123", # 主体(Subject) "iss": "https://auth.your-app.example.com", # 発行者(Issuer) "aud": "your-api", # 受信者(Audience) "iat": 1757000000, # 発行時刻(Issued At) "exp": 1757003600, # 有効期限(Expiration) ← 必須 "jti": "unique-token-id" # JWT ID(リプレイ攻撃対策) }
5. 機密情報をペイロードに含めない
JWTペイロードは暗号化されていないことを常に意識します。パスワード・クレジットカード番号・個人情報・内部システムのパスは絶対に含めません。認可判断に必要最小限のクレームのみに絞ります。
中小企業でも今日からできること
JWTの脆弱性対策で、すぐに着手できる確認項目をまとめます。
| 確認項目 | 確認方法 | 優先度 |
|---|---|---|
| algフィールドがサーバー固定か | ライブラリのverify/decodeオプションを確認 | 高(即対応) |
| “none”アルゴリズムを拒否しているか | algに”none”を入れたJWTを送信してテスト | 高(即対応) |
| HS256のシークレットキーが32バイト以上か | 環境変数・設定ファイルで鍵長を確認 | 高(即対応) |
| expクレームが設定されているか | 発行JWTをデコードして確認(jwt.io等) | 高 |
| ペイロードに機密情報がないか | 発行JWTのペイロードを全項目レビュー | 中 |
| 使用ライブラリに既知のJWT脆弱性がないか | npm audit / pip-audit / Dependabotで確認 | 中 |
ライブラリの選定も重要です。JWTの取り扱いは、自前実装ではなく実績のあるライブラリ(Node.js: jose / jsonwebtoken、Python: PyJWT、Java: java-jwt / nimbus-jose-jwt)を使い、定期的にアップデートを適用します。脆弱なコンポーネントの使用はOWASP Top 10(A06)でも指摘される重大リスクです。詳細は姉妹サイトLinuxMaster.JPでも依存関係管理のベストプラクティスを解説しています。
よくある誤解と注意点
「JWTはBase64で暗号化されているから安全」という誤解
Base64URLエンコードは暗号化ではなく、エンコード(変換)です。誰でもデコードしてペイロードを読めます。機密情報の保護には、JWE(JSON Web Encryption)を使うか、機密データをペイロードから除外するのが正しい対応です。
「JWTをローカルストレージに保存しても問題ない」という誤解
ローカルストレージに保存したJWTはXSSで盗まれます。セキュリティ的にはHttpOnly・Secure属性付きのCookieに保存するほうが安全です。XSSによるアカウント乗っ取りはアカウントテイクオーバー(ATO)攻撃の典型的な経路の一つです。
「有効期限が短いJWTなら脆弱性の影響は小さい」という誤解
有効期限の短さはリスク低減に寄与しますが、リプレイ攻撃の完全な対策にはなりません。jtiクレームをサーバー側で管理し、同じJWT IDの再利用を検知する実装が必要です。
「algに”none”を試しても実際には使えない」という誤解
古いバージョンのライブラリや自前実装では今でも通ってしまうケースがあります。セキュリティスキャナーは必ずnone攻撃を試みるため、自分のアプリで実際にテストして確認することを勧めます。

本記事のまとめ
JWTは正しく実装すれば堅牢な認証基盤になりますが、署名検証の甘さやアルゴリズムの取り扱いを誤ると権限昇格・認証回避につながります。
| 攻撃手法 | 根本原因 | 対策 |
|---|---|---|
| noneアルゴリズム | クライアント指定のalgを信頼 | サーバー側でalg固定・none拒否 |
| アルゴリズム混乱(RS256→HS256) | アルゴリズム切り替えを許容 | 検証時に1つのalgのみ指定 |
| ブルートフォース | シークレットキーが短い・弱い | 256bit以上のランダムキー使用 |
| jkuインジェクション | 外部JWKSを無検証で取得 | JWKS URLをサーバー側で固定 |
| 情報漏洩 | ペイロードに機密情報を含む | 最小限クレームのみに絞る |
| リプレイ攻撃 | JTIの再利用チェックがない | jtiをサーバー側で記録・検証 |
実装が正しくても、ライブラリの古いバージョンに既知のJWT脆弱性が残っている場合があります。セキュアコーディングの観点から、定期的な依存関係の更新と脆弱性スキャンをセットで実施してください。
PR
体系的に学ぶ 安全なWebアプリケーションの作り方 第2版(徳丸浩/SBクリエイティブ)
JWT脆弱性を含むWebアプリ全般の脆弱性を体系的に解説した定番書。認証・セッション管理の落とし穴から実装レベルの対策まで、開発者・情シスを問わずすすめられる一冊です。
