JWT セキュリティのベストプラクティス:攻撃、防御、そして 2026 年版チェックリスト
JSON Web Token は現代の認証の大半を支えている。それなのに、JWT を安全に保つためのベストプラクティスは、本来よりずっと頻繁に飛ばされている。JWT は OAuth 2.0、OpenID Connect、マイクロサービス内のサービス間呼び出しで事実上の標準クレデンシャル形式になった。その一方で、毎年とぎれずに CVE を生み続けてもいる。そのほとんどは、同じ避けられたはずのミスに行き着く。署名のないトークンを受け入れる、攻撃者が選んだアルゴリズムを信用する、弱い署名シークレットを使う、クレーム検証を省く、といったものだ。
JWT が安全になるのは、4 つの条件がそろったときだけだ。署名が破られていない。攻撃者にアルゴリズムをすり替えられない。クレームが実際に検証されている。トークンが簡単には盗まれない場所に保管されている。このどれか 1 つでも崩れれば、堅牢化された API ではなく、ただの認証バイパスになる。本ガイドでは、まず重要な攻撃 3 つを取り上げ、続いて防御策として、アルゴリズムの選択と固定、鍵の管理、クレームの検証、トークンの保管を順に見ていく。最後は、レビューにそのまま貼り付けて使えるチェックリストで締めくくる。
JWT の署名が実際に守ってくれるもの(そして守ってくれないもの)
どの攻撃を理解するにも、まず 1 つの事実をはっきりさせておきたい。JWT はエンコードされているだけで、暗号化されてはいない。署名付きトークンは、ドットでつないだ 3 つの Base64URL セグメント、つまり header.payload.signature でできている。ヘッダーとペイロードは JSON を素朴に Base64URL エンコードしただけだ。トークンを手にすれば、誰でも中のクレームを全部読める。試しに任意のトークンを JWT デコーダー に貼り付けると、ヘッダーとペイロードが鍵なしで読める JSON に展開される。ペイロードは設計上、公開されている。
では、セキュリティはどこから来るのか。署名だ。署名だけだ。署名は、シークレット(HMAC)または秘密鍵(RSA、ECDSA)を使って、ヘッダーとペイロードから計算される暗号学的な値である。攻撃者はトークンを自由に読めても、署名鍵がなければ検証を通る別のトークンは作れない。信頼モデルはこの一点に尽きる。
ここから 2 つのことが言える。まず、ペイロードにはシークレットを入れてはならない。パスワード、API キー、PII の類だ。トークンを傍受した相手に全部読まれてしまう。次に、セキュリティ態勢全体が、署名を正しく検証するというたった 1 つのステップにかかっている。そして攻撃者が狙うのも、まさにそのステップだ。トークンをセグメントごとに読み解く手順をもっと詳しく知りたければ、JWT のデコード方法 を参照してほしい。
重大な JWT 攻撃 3 種(とそれぞれの止め方)
JWT の脆弱性の大半は、同じテーマの変奏だ。サーバーが、攻撃者の手の届くところにある何かを信用してしまう。ここでは、認証を真っ向から破る 3 つを、仕組みと直し方をセットで取り上げる。
1. alg:none 攻撃 — 署名なしトークンによるバイパス
JWS 仕様には none という alg の値があり、「署名なし」を意味する。alg:none のトークンは署名セグメントが空で、それでも末尾のドットは残るので、header.payload. のような形になる。攻撃は単純だ。有効なトークンを取り、ヘッダーの alg を none に書き換え、好きなクレーム(たとえば "role": "admin")に差し替えて、署名を取り去る。初期の JWT ライブラリはこれをデフォルトで受け入れていたので、偽造トークンがそのまま検証を通り抜けた。鍵もなし、署名もなし、それでなりすませてしまった。
そうしたトークンがどんな形になるかは、JWT デコーダー で「alg:none」の例を読み込んでみればわかる。デコーダーは、そのトークンが署名されておらず認証に使ってはならない旨を、赤い警告ではっきり表示する。自分で 1 つ再現してみると、脅威の中身が 1 分で腑に落ちる。
防御策は、検証呼び出しごとに、許可するアルゴリズムを明示的なリストで指定することだ。何を許すかをライブラリのデフォルト任せにしてはいけない。古いデフォルトは緩く、明示するコストはオプション 1 つ分でしかない。
// WRONG — the library may accept alg:none or any algorithm
jwt.verify(token, key);
// RIGHT — pin the exact algorithm you expect
jwt.verify(token, key, { algorithms: ['RS256'] });
none がこの配列に現れることがあってはならない。アルゴリズムを固定できないライブラリなら、別のものに取り替えよう。
2. アルゴリズム混同 — RS256 を HS256 へダウングレード
これは実務上もっとも危険な JWT の脆弱性だ。2015 年から知られているのに、今日の監査でもまだ見つかる。狙われるのは、ヘッダー内の alg フィールド、つまりトークンのうち攻撃者が書き換えられる唯一の部分を見て、どう検証するかを決めてしまうサーバーだ。
仕組みはこうだ。あなたのサーバーは RS256 トークンを発行する。RSA の秘密鍵で署名し、対応する公開鍵で検証する。その公開鍵は定義からして公開されていて、JWKS エンドポイントに置かれていたり、リポジトリに入っていたりする。攻撃者はそれを手に入れ、トークンのヘッダーを RS256 から HS256 に書き換え、公開鍵の文字列を HMAC シークレットに使って HMAC-SHA256 で偽造ペイロードに署名する。検証側のあなたのコードが、ヘッダーの alg を読んでそれに合わせて HMAC を選ぶと、同じ公開鍵をシークレットとしてトークンに HMAC-SHA256 を計算する。署名は一致し、偽造トークンが受理される。
原因は 2 つの事実が衝突したことだ。検証側が攻撃者の手の届く alg ヘッダーを信用した。そして RSA の公開鍵が、攻撃者から見て HMAC 鍵として使える状態にあった。どちらも、単独ではバグではない。公開鍵は公開されているべきもので、alg ヘッダーはトークンを記述するべきものだ。脆弱性が生まれるのは、検証ロジックがそのヘッダーに鍵種別とアルゴリズムを選ばせてしまった瞬間だ。攻撃者が書き込んだ値が、サーバーの暗号処理の経路そのものを動かしてしまうからである。
// WRONG — verification method follows the header's alg field
jwt.verify(token, publicKeyOrSecret);
// RIGHT — hard-code the expected algorithm; never let the header choose
jwt.verify(token, publicKey, { algorithms: ['RS256'] });
非対称アルゴリズムを明示的に固定し(RS256 か ES256 だけ)、HMAC 検証を RSA 検証とは別のコード経路に保ち、鍵種別をきちんと区別する、メンテされたライブラリを使うこと。JWT デコーダー が HS 系のトークンに公開鍵混同の警告を付けるのも、この攻撃がそれだけありふれているからだ。非対称であるはずのトークンが HS256 で現れたら、その警告がサインになる。
3. 弱い HMAC シークレット — 総当たりと辞書攻撃
HMAC(HS256/384/512)を使う場合、トークンのセキュリティはすべて、たった 1 つのシークレットのエントロピーにかかる。そのシークレットが短かったり、辞書語だったり、secret や password123 のような値だったりすると、有効なトークンを 1 つ手に入れた攻撃者は、それをオフラインで解読できる。hashcat のようなツールは、トークンの署名に対して 1 秒あたり数十億の候補を試す。シークレットが破られれば、攻撃者は好きなトークンを何でも発行できる。有効な admin クレデンシャルを永久に持つことになる。
この攻撃が静かに危ないのは、完全にオフラインで終わる点だ。攻撃者はログインエンドポイントを叩かないので、引っかかるレート制限もなく、ログに残って気づける痕跡もない。トークンを 1 つ手に入れ、自分のハードウェアでシークレットを解読し、あらゆる検証を通るトークンに署名できるようになって、初めて戻ってくる。だから修正は譲れない。暗号学的に安全な情報源から少なくとも 32 バイト(256 ビット)のランダム値を使い、それをコードやリポジトリではなくシークレットマネージャーに保管すること。
// WRONG — guessable, low entropy, crackable in seconds
const secret = "password123";
// RIGHT — 256 bits from a CSPRNG, then load from KMS at runtime
const secret = require('crypto').randomBytes(32).toString('base64');
強い値をすぐ用意したいなら、ランダムパスワード生成ツール が HMAC 鍵に向いた高エントロピーの文字列を生成してくれる。違いを実際に確かめたいなら、JWT エンコーダー で強いシークレットを使ってテスト用トークンに署名してみてほしい。処理はすべてブラウザ内で完結するので、シークレットがマシンの外に出ることはない。なお、検証が信頼境界をまたぐとき、つまり複数のサービスやサードパーティの検証者が関わるときは、HS256 はやめて、次に取り上げる非対称アルゴリズムへ移ろう。
適切なアルゴリズムの選択と固定
混同攻撃の勝敗はアルゴリズムの選択で決まる。だから意図を持って選ぼう。実際に使うのは次の 3 つだ。
| アルゴリズム | 種別 | 署名/検証に使う鍵 | 使いどころ |
|---|---|---|---|
| HS256 | 対称(HMAC) | 1 つの共有シークレット | 単一の信頼境界、同一の当事者が署名と検証を行う |
| RS256 | 非対称(RSA) | 秘密鍵で署名/公開鍵で検証 | サービス間、サードパーティによる検証、JWKS ローテーション |
| ES256 | 非対称(ECDSA) | 秘密鍵で署名/公開鍵で検証 | RS256 と同じ用途、より小さく高速な鍵 — 新規システムに推奨 |
判断は単純だ。同じ当事者が 1 つの信頼境界の内側で署名も検証も行うなら、HS256 で十分だし速い。署名者以外の誰か、別のサービスやパートナー、公開クライアントが検証するなら、非対称アルゴリズムを使い、ES256 を優先しよう。同じ強度なら、鍵も署名も RSA よりかなり小さくなる。JWT エンコーダー では、HS256、RS256、ES256 のサンプルトークンを並べて署名し、構造と署名長を見比べられる。
何を選んでも、効いてくる防御は同じだ。検証呼び出しで許可するアルゴリズムの集合を 1 つ明示的に固定し、ヘッダー内の alg フィールドを信用しないこと。許可リストは、ほかのすべてが乗る土台になる。
鍵の管理とローテーション
アルゴリズムは、背後にある鍵の強さまでしか安全になれない。そして鍵の扱いこそ、多くのガイドが触れずに済ませるところだ。HS256 では、シークレットは少なくとも 32 バイトのランダム値にして、シークレットマネージャー(AWS Secrets Manager、HashiCorp Vault、Azure Key Vault)に置く。非対称アルゴリズムでは、秘密鍵は HSM または KMS に置き、アプリケーションコードには触れさせない。公開鍵は公開してよく、ふつうは検証者が取得する JWKS エンドポイントから配信する。
ローテーションは緊急対応ではなく日常にしておきたい。各鍵を JWT ヘッダーの kid(鍵 ID)でタグ付けして、どの鍵がそのトークンに署名したかを検証者が判別できるようにする。検証側では有効な鍵を小さな集合に保つ。現在の鍵に加えて 1 つ前の鍵を持っておけば、ローテーション直前に署名されたトークンも、有効期間のあいだはまだ検証できる。この重なりがあるおかげで、ローテーションが障害にならずに済む。
鍵に関する短いチェックリスト。
- 署名鍵は少なくとも 90 日ごとにローテーションし、侵害の疑いがあれば直ちにローテーションする。
- 公開鍵は JWKS 経由で配信し、
kidでバージョン管理する。 - 秘密鍵と HMAC シークレットは KMS または HSM に保管する — git に入れない、クライアントコードに入れない、ハードコードしない。
- 漏洩時には、鍵をローテーションし、未失効のリフレッシュトークンを一度に失効させる。
省いてはならないクレーム検証
署名の検証は、トークンが本物だと証明する。だが、そのトークンが今、あなたのためのものだとまでは証明しない。それを担うのがクレーム検証で、追加できる防御のなかで一番安上がりだ。すべてのリクエストで確認すべきクレームは 5 つある。
exp(有効期限)— 有効期限が過去のトークンは拒否する。nbf(not before)— 有効期間が始まる前に使われたトークンは拒否する。iat(issued at)— 必要に応じて、ありえないほど古いトークンを拒否する。iss(発行者)— トークンが、あなたが信頼する発行者から来たことを確認する。aud(オーディエンス)— トークンがあなたのサービス向けに発行されたものであることを確認する。audのチェック漏れは最もよくある静かな穴で、ある API 向けに発行されたトークンが別の API に対して再利用されるのを許してしまう。
ほとんどのライブラリは、期待値を渡せばこれらを代わりに検証してくれる。
jwt.verify(token, key, {
algorithms: ['ES256'],
issuer: 'https://auth.example.com',
audience: 'api.example.com',
clockTolerance: 5, // seconds, for distributed clock skew
});
小さなクロック許容差は認めておく。5 秒あたりが一般的だ。これでサーバー間のわずかなずれのせいで、本来有効なトークンが拒否されることはなくなる。ただし、これを大きくしたくなっても我慢しよう。許容差を広げると、有効期限切れのトークンがまだ通る窓も広がる。exp はまさにその窓を閉じるためにある。トークンの exp や iat の値をざっと目で確かめたいときは、JWT デコーダー に放り込み、Unix タイムスタンプ変換ツール でタイムスタンプを変換するとよい。
トークンの寿命と JWT の保管場所
サーバー側のチェックは話の半分でしかない。クライアントがトークンをどこに保管するかで、それがどれだけ簡単に盗まれるかが決まる。保管場所は、XSS とセッションハイジャックがぶつかる地点でもある。持ちこたえるパターンはこうだ。短命のアクセストークン(15〜60 分)を、別の、より長命で失効できるリフレッシュトークンと組み合わせる。
保管場所をどうするかは、結局 1 つのトレードオフに行き着く。
| 保管場所 | XSS への露出 | CSRF リスク | 推奨 |
|---|---|---|---|
| localStorage | 高 — ページ上のあらゆる JavaScript が読み取れる | なし | セッショントークンには避ける |
| HttpOnly + Secure + SameSite=Strict クッキー | 低 — JavaScript からは見えない | CSRF 対策が必要 | セッションに推奨 |
localStorage 内のトークンは、ページ上で動くどのスクリプトからも読める。だから XSS バグが 1 つあればセッション全体が漏れ、攻撃者はそれが生きている限り、自分のマシンから使い回せる。HttpOnly クッキーは JavaScript から読めないので、XSS の被害は、攻撃者が生きているページの中でできることにとどまる。それでもまずいが、持ち去れるクレデンシャルにはならない。クッキー方式の代償は、CSRF 対策が新たに要ることだ。クッキーはすべてのリクエストに自動で付いていくからである。SameSite=Strict に CSRF トークンを足せば対処できる。アクセストークンは短く保って漏洩の影響範囲を抑え、リフレッシュトークンは HttpOnly、Secure、SameSite のクッキーに入れる。ログアウト時や侵害が疑われるときは、リフレッシュトークンをサーバー側で失効させ、署名鍵をローテーションする。XSS や CSRF、セキュアなクッキーまわりの広い話は、Web セキュリティのベストプラクティス ガイドを参照してほしい。
JWT セキュリティチェックリスト
JWT ベースの認証をリリースする前に、これを一通り確認すること。
- 検証が明示的なアルゴリズム許可リストを固定し、
alg:noneを拒否している。 - 非対称検証が期待するアルゴリズムをハードコードし、ヘッダーから
algを決して読まない(混同を防ぐ)。 - HS256 のシークレットは少なくとも 32 バイトのランダム値で、KMS から読み込んでいる。
- 秘密鍵は HSM/KMS に置き、公開鍵は JWKS 経由で配信し
kidでバージョン管理している。 - 署名鍵は少なくとも 90 日ごとにローテーションしている。
- すべてのリクエストが
exp、nbf、iat、iss、audを検証し、クロック許容差は 5 秒以下である。 - アクセストークンは 15〜60 分持続し、リフレッシュトークンは
HttpOnlyクッキーに入っている。 - ペイロードにシークレットがない — それはエンコードであって暗号化ではない。
FAQ
JWT はデフォルトで安全ですか?
いいえ。JWT のセキュリティは設定次第です。アルゴリズムを固定し、alg:none を拒否し、高エントロピーのシークレットまたは鍵を使い、クレームを検証する。これらを自分でやる必要があります。デフォルトや緩いライブラリ設定のままだと、認証バイパスを許してしまうことがよくあります。
最も危険な JWT の脆弱性は何ですか?
アルゴリズム混同です。RS256 を HS256 へダウングレードし、公開鍵を HMAC シークレットに使うものです。2015 年から知られているのに、いまだに監査で見つかります。ヘッダーの alg を見て検証方法を選んでしまうサーバーを突くからです。
HS256 と RS256 のどちらを使うべきですか?
同じ当事者が 1 つの信頼境界の内側で署名と検証を行うなら、HS256 で構いません。別のサービスやサードパーティが検証する場合、あるいは JWKS ローテーションが必要な場合は、RS256 か ES256 を使います。新規システムなら ES256 を優先しましょう。同じ強度で、より小さく速い鍵が得られます。
JWT はどこに保管すべきですか?
セッショントークンには HttpOnly、Secure、SameSite のクッキーを優先しましょう。JavaScript から読めないので、XSS バグ 1 つで盗まれることがありません。セッショントークンに localStorage を使うのは避けてください。どんな XSS でもセッション全体が漏れ、使い回されてしまいます。
JWT の署名鍵はどのくらいの頻度でローテーションすべきですか?
日常運用では少なくとも 90 日ごとに、侵害が疑われるときは直ちにローテーションします。鍵は kid でバージョン管理し、検証側にはアクティブな鍵と 1 つ前の鍵の両方を持たせておくと、ローテーション直前に署名されたトークンもまだ検証できます。
JWT は改ざんできますか?
署名鍵がなければ改ざんできません。検証を通るトークンを偽造できる攻撃者はいません。ただし、サーバーが alg:none を受け入れたり、アルゴリズム混同に弱かったり、弱いシークレットを使っていたりすると、署名のチェックそのものを回避されることがあります。これは設定の失敗であって、JWT そのものの欠陥ではありません。
どのクレームを検証しなければなりませんか?
exp(有効期限)、nbf(not before)、iat(issued at)、iss(発行者)、aud(オーディエンス)を検証します。なかでも aud のチェック漏れは最もよくある静かな脆弱性で、あるサービス向けのトークンが別のサービスで使い回されるのを許してしまいます。
まとめ
JWT のセキュリティは複雑ではないが、どの層も漏れなく持ちこたえる必要がある。署名は唯一の保証だから、正しく検証する。アルゴリズムを 1 つ明示的に固定し、ヘッダーの alg を信用しない。KMS に置いた、定期的にローテーションする強い鍵を使う。すべてのリクエストで exp、nbf、iat、iss、aud を検証する。トークンは XSS が届かない場所に保管する。
手を動かすなら、まず任意のトークンを JWT デコーダー に貼り付けてアルゴリズムとクレームを確認し、alg:none や HS 混同のリスクを拾ってみるとよい。署名そのものを試したいときは JWT エンコーダー を使えば、完全にブラウザ内で実験できる。鍵がデバイスの外に出ることはない。