JWT の構造と署名検証・典型的な脆弱性
alg:none や HS/RS 鍵混同で「検証している」つもりが素通りになる——JWT の三部構成と署名検証の正しい順序、kid インジェクションや失効設計の落とし穴を原理から押さえ、自前検証の事故を防げる。
- 1.JWT は header.payload.signature を Base64URL で連結した文字列。署名は header+payload を対象に計算し、ペイロード自体は暗号化されない(JWS は署名のみ、機密が要るなら JWE)。Base64 は誰でも復号できる。
- 2.古典的脆弱性は alg をトークン側に信用すること。alg:none で署名を空にする、HS256 を期待するサーバーに RS256 の公開鍵を鍵に使わせる HS/RS 鍵混同、kid に SQL/パス/コマンドを注入——いずれもアルゴリズムと鍵を「サーバー側で固定」すれば防げる。
- 3.ステートレスゆえに即時失効ができない。短い exp + リフレッシュトークン + サーバー側の失効リスト(jti ブラックリスト/トークンバージョン)で、有効期限と失効可能性のトレードオフを設計する。
JWT は「署名付きの自己記述トークン」である
JWT(JSON Web Token、RFC 7519)は、クレーム(claim、主張)を JSON で表し、改ざんを検知できる署名を付けた文字列です。サーバーがセッションを持たずに「このトークンは確かに自分が発行し、改ざんされていない」と検証できるため、ステートレスな認証・認可で広く使われます。誰が何をしてよいかという土台は 認証と認可の違い に譲り、ここでは JWT そのものの内部構造と、検証実装で繰り返し起きる事故を原理から扱います。
実体は3つの部分を .(ドット)で連結した文字列です。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 . eyJzdWIiOiIxMjM0Iiwicm9sZSI6InVzZXIifQ . dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
└──────────── header ────────────┘ └────────── payload ──────────┘ └──────────── signature ────────────┘
- header:署名アルゴリズム
alg(例HS256,RS256)とトークン型typを持つ JSON。 - payload:クレームの集合。標準クレームに
iss(発行者)sub(主体)aud(受信者)exp(有効期限)iat(発行時刻)jti(一意 ID)などがある。 - signature:
headerとpayloadの Base64URL 表現を.で連結した文字列に対して計算した署名。
各部は Base64URL エンコードであって暗号化ではありません。payload をデコードすれば中身は誰でも読めます。機密情報を payload に入れてはいけないのはこのためです。
eyJ... で始まるのは {" を Base64URL したものというだけで、鍵も秘密もありません。payload に個人情報・内部 ID・権限の生データを入れると、署名で守られているのは「改ざん検知」だけなので中身は丸見えです。署名(JWS)は完全性と認証は与えますが機密性は与えません。
JWS と JWE:守るものが違う
「JWT」と一括りにされがちですが、トークンの保護方式には2系統あります。
| 観点 | JWS(署名) | JWE(暗号化) |
|---|---|---|
| 守るもの | 完全性・認証(改ざん検知・発行元証明) | 機密性(中身を秘匿)+完全性 |
| payload は読めるか | 読める(Base64URL のみ) | 読めない(暗号文) |
| 構成 | 3部(header.payload.signature) | 5部(header.encKey.iv.ciphertext.tag) |
| alg の意味 | 署名アルゴリズム(HS256/RS256 等) | 鍵管理アルゴリズム(RSA-OAEP 等)+ enc |
| 主用途 | 認証トークン・ID トークン(大多数) | 中身を見せたくないトークン |
実務で「JWT」と呼ばれるトークンの大半は JWS です。中身を秘匿したいなら JWE を使うか、そもそも payload に秘密を入れない設計にします。署名アルゴリズムの内部は デジタル署名スキームの内部、HMAC を使う HS 系の土台は MAC と HMAC を参照してください。
署名検証の正しい順序
検証は「署名が正しいか」だけでは不十分で、順序と範囲が肝心です。最低限こうします。
1. ドットで3分割し、header と payload を Base64URL デコードする
2. サーバーが許可するアルゴリズムか確認する(★トークンの alg を信用しない)
3. そのアルゴリズムと「サーバーが選んだ鍵」で署名を再計算し、定数時間比較する
4. 署名が正しい時に限り、クレームを検証する:
exp(期限切れでないか)/ nbf(有効開始前でないか)/ iat
iss(期待する発行者か)/ aud(自分宛てか)
5. すべて通って初めて payload のクレーム(role 等)を信頼する
決定的に重要なのは 手順2と3で「アルゴリズムと鍵をサーバー側が決める」 ことです。次に挙げる脆弱性は、ほぼすべて「トークンに書いてある alg をそのまま使ってしまう」ことに起因します。
alg:none — 署名を「無い」ことにする
JWS 仕様には署名なしを表す alg: "none" が存在します。素朴な検証ライブラリは、header の alg を読んでそのアルゴリズムで検証しようとするため、攻撃者が header を {"alg":"none"} に書き換え、署名部を空文字にしたトークンを送ると——none の検証は「署名チェックをしない」ので——素通りします。
攻撃者が作るトークン:
header = {"alg":"none","typ":"JWT"}
payload = {"sub":"1234","role":"admin"} ← 権限を勝手に昇格
signature = (空)
→ alg を信用する検証は「none だから検証スキップ」で受理してしまう
防御は単純で、検証時に許可するアルゴリズムをサーバーが明示すること。verify(token, key, {algorithms: ['RS256']}) のように許可リストを渡し、none を絶対に許可しない。トークンの alg を「どの鍵・どの方式で検証するかの指示」として読んではいけません。alg はあくまで参考情報で、実際に使う方式と鍵は設定で決め打ちします。
HS/RS 鍵混同 — 公開鍵を HMAC 鍵にすり替える
より巧妙なのが、非対称(RS256)と対称(HS256)の取り違えを突く 鍵混同(key confusion) です。RS256 は秘密鍵で署名し公開鍵で検証する非対称方式、HS256 は秘密の共有鍵で HMAC を計算する対称方式です。署名検証 API が「鍵」と「方式」を分離していないと事故が起きます。
サーバーが RS256 を想定し、検証関数に RSA 公開鍵をそのまま渡しているとします。攻撃者は header を HS256 に書き換え、公開鍵の文字列を HMAC の共有鍵として 署名を計算します。検証側は header の alg=HS256 を信じ、手元の「鍵(=公開鍵の文字列)」で HMAC を検証してしまう——公開鍵は誰でも入手できるので、攻撃者は任意の payload に正しい署名を付けられます。
正規サーバー: verify(token, rsaPublicKey) ← 方式を固定していない
攻撃者:
header = {"alg":"HS256"} ← RS256 から HS256 へ
signature = HMAC-SHA256(rsaPublicKey, header.payload) ← 公開鍵を鍵に流用
→ サーバーは alg=HS256 として「公開鍵で HMAC 検証」し、一致してしまう
根本原因は header の alg に従って方式を切り替えてしまう こと。ここでも対策は同じで、検証で使うアルゴリズムを ['RS256'] に固定し、HS と RS を一つの鍵オブジェクトで兼用しないことです。鍵には用途(署名検証専用・対称鍵専用)を型として持たせ、方式とセットで扱います。
kid インジェクション — 鍵 ID を信用しすぎる
header の kid(Key ID)は「どの鍵で署名したか」を示す任意の文字列で、複数鍵のローテーションに使われます。検証側がこの kid を使って鍵を引く処理が雑だと、注入の入口になります。
- パストラバーサル:
kidをファイルパスとして鍵を読む実装なら、kid: "../../dev/null"のような値で中身が空の「鍵」を選ばせ、空鍵で HMAC を成立させる。 - SQL インジェクション:
SELECT key FROM keys WHERE id = '<kid>'にkidを直挿しすると、kidに SQL を仕込んで任意の鍵文字列を返させ、その鍵で署名を偽造できる。 - コマンドインジェクション:
kidをシェルに渡す実装なら任意コマンド実行に至る。
kid をそのまま使う危険な実装:
key = readFile("/keys/" + header.kid) ← パストラバーサル
key = db.query("... WHERE id='" + header.kid + "'") ← SQLi
kid は攻撃者が自由に書ける値です。許可された鍵 ID の集合に対する完全一致で引き、パス・SQL・コマンドの文字列として解釈しないこと。鍵はあらかじめ登録した固定の集合(許可リスト)からのみ選び、未知の kid は即拒否します。SQL を使うならプレースホルダ(バインド変数)で渡します。
JWKS(公開鍵を URL で配る仕組み)を使う場合は、jku/x5u のような header 内の URL を信用して鍵を取りに行かない ことも重要です。攻撃者が自分のサーバーの公開鍵を指す URL を仕込めば、対応する秘密鍵で署名した偽トークンが通ってしまいます。鍵の取得元は設定で固定します。証明書チェーンを使う場合の検証は X.509 証明書チェーンの検証 が詳しいです。
失効と有効期限:ステートレスの代償
JWT の利点である「サーバーがセッションを持たない」ことは、そのまま 発行済みトークンを即座に無効化できない という弱点になります。サーバーはトークンを検証するだけで状態を持たないので、ログアウトや権限剥奪、漏洩が起きても、exp が来るまでそのトークンは有効なままです。
現実的な設計は、相反する要求のトレードオフです。
| 設計 | 利点 | 代償 |
|---|---|---|
| exp を長くする | 再ログイン頻度が下がる(UX 良) | 漏洩トークンが長く生き続ける |
| exp を短くする | 漏洩の影響時間を最小化 | 頻繁な再発行・再ログインが要る |
| リフレッシュトークン併用 | アクセストークンは短命、更新は別経路 | リフレッシュトークンの保管・失効を別途設計 |
| 失効リスト(jti/version) | 即時失効が可能になる | サーバー側状態が必要(ステートレス性が薄れる) |
定石は 短命のアクセストークン(数分〜十数分)+ リフレッシュトークン です。アクセストークンは短い exp で漏洩の影響を限定し、期限切れ後はリフレッシュトークンで更新します。即時失効が要る場合は完全なステートレスを諦め、サーバー側に最小の状態を持ちます。
- jti ブラックリスト:失効させたいトークンの
jtiを保存し、検証時に照合する。期限切れまで保持すればよいので無限には増えない。 - トークンバージョン:ユーザーごとにバージョン番号を持ち、payload の値と一致しなければ拒否。パスワード変更や「全端末からログアウト」でバージョンを上げれば、既発行トークンを一括失効できる。
資格試験でも実務でも問われるのは「JWT を使えばステートレスで失効も完璧」という誤解です。正しくは、即時失効・強制ログアウト・きめ細かいセッション管理が要件なら、サーバー側セッション(あるいは失効リスト併用)の方が素直です。JWT が真価を発揮するのは、短命トークンで足り、サービス間でトークンを持ち回りたい場面。要件に「即時失効」が含まれるなら、ステートレスへの固執は事故の元です。
まとめ
JWT は header.payload.signature を Base64URL で連結した自己記述トークンで、署名は改ざんを検知するが中身は秘匿しない(機密が要るなら JWE)。検証の急所は アルゴリズムと鍵をサーバー側で固定することに尽きます。これを守らないと、alg:none で署名が素通りし、HS/RS 鍵混同で公開鍵が HMAC 鍵に化け、kid 経由でパス・SQL・コマンドが注入されます。さらにステートレスゆえ即時失効ができず、短い exp とリフレッシュトークン、必要なら jti 失効リストやトークンバージョンで、有効期限と失効可能性のトレードオフを設計する必要があります。署名そのものの強度は デジタル署名スキームの内部 と MAC と HMAC、認可の土台は 認証と認可の違い と合わせて押さえると、JWT を「正しく検証する」実装が原理から判断できるようになります。
セキュリティ Article
JWT の構造と署名検証・典型的な脆弱性を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
JWT
比較で見る軸
難易度: advanced / カテゴリ: セキュリティ / タグ数: 5
導入後に効く点
古典的脆弱性は alg をトークン側に信用すること。alg:none で署名を空にする、HS256 を期待するサーバーに RS256 の公開鍵を鍵に使わせる HS/RS 鍵混同、kid に SQL/パス/コマンドを注入——いずれもアルゴリズムと鍵を「サーバー側で固定」すれば防げる。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- セキュリティ
- タグ数
- 5
判断チェックリスト
- 自社の用途が「JWT / JWS」に近いか確認する。
- 強みである「JWT は header.payload.signature を Base64URL で連結した文字列。署名は header+payload を対象に計算し、ペイロード自体は暗号化されない(JWS は署名のみ、機密が要るなら JWE)。Base64 は誰でも復号できる。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。