TL

JWT の構造と署名検証・典型的な脆弱性

alg:none や HS/RS 鍵混同で「検証している」つもりが素通りになる——JWT の三部構成と署名検証の正しい順序、kid インジェクションや失効設計の落とし穴を原理から押さえ、自前検証の事故を防げる。

応用JWTJWSトークン認証署名検証Webセキュリティ最終更新: 2026-06-21
TL;DR要点だけ先に
  • 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)などがある。
  • signatureheaderpayload の Base64URL 表現を . で連結した文字列に対して計算した署名。

各部は Base64URL エンコードであって暗号化ではありません。payload をデコードすれば中身は誰でも読めます。機密情報を payload に入れてはいけないのはこのためです。

Base64 は「暗号化」ではない

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 は「信用できない入力」として扱う

kid は攻撃者が自由に書ける値です。許可された鍵 ID の集合に対する完全一致で引き、パス・SQL・コマンドの文字列として解釈しないこと。鍵はあらかじめ登録した固定の集合(許可リスト)からのみ選び、未知の kid は即拒否します。SQL を使うならプレースホルダ(バインド変数)で渡します。

JWKS(公開鍵を URL で配る仕組み)を使う場合は、jkux5u のような header 内の URL を信用して鍵を取りに行かない ことも重要です。攻撃者が自分のサーバーの公開鍵を指す URL を仕込めば、対応する秘密鍵で署名した偽トークンが通ってしまいます。鍵の取得元は設定で固定します。証明書チェーンを使う場合の検証は X.509 証明書チェーンの検証 が詳しいです。

失効と有効期限:ステートレスの代償

JWT の利点である「サーバーがセッションを持たない」ことは、そのまま 発行済みトークンを即座に無効化できない という弱点になります。サーバーはトークンを検証するだけで状態を持たないので、ログアウトや権限剥奪、漏洩が起きても、exp が来るまでそのトークンは有効なままです。

現実的な設計は、相反する要求のトレードオフです。

設計利点代償
exp を長くする再ログイン頻度が下がる(UX 良)漏洩トークンが長く生き続ける
exp を短くする漏洩の影響時間を最小化頻繁な再発行・再ログインが要る
リフレッシュトークン併用アクセストークンは短命、更新は別経路リフレッシュトークンの保管・失効を別途設計
失効リスト(jti/version)即時失効が可能になるサーバー側状態が必要(ステートレス性が薄れる)

定石は 短命のアクセストークン(数分〜十数分)+ リフレッシュトークン です。アクセストークンは短い exp で漏洩の影響を限定し、期限切れ後はリフレッシュトークンで更新します。即時失効が要る場合は完全なステートレスを諦め、サーバー側に最小の状態を持ちます。

  • jti ブラックリスト:失効させたいトークンの jti を保存し、検証時に照合する。期限切れまで保持すればよいので無限には増えない。
  • トークンバージョン:ユーザーごとにバージョン番号を持ち、payload の値と一致しなければ拒否。パスワード変更や「全端末からログアウト」でバージョンを上げれば、既発行トークンを一括失効できる。
JWT は万能ではない

資格試験でも実務でも問われるのは「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、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
  • 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。

次に確認する観点

JWTJWSトークン認証署名検証WebセキュリティJWTJWSトークン認証
参考: 公式情報