JWT検証でalgを信用してはいけない理由:none、RS256→HS256の取り違えとアルゴリズム固定

JWTヘッダーのalgはトークンを作った側が書く値です。alg noneとRS256→HS256の取り違えが通る理由、RFC 8725が求めるアルゴリズム固定と「1つの鍵に1つのアルゴリズム」、署名の後に確認するiss・aud・exp・typをまとめました。

開発セキュリティWeb

JWTの先頭部分であるヘッダーにはalgが入っていて、どのアルゴリズムで署名したかを示します。問題は、この値を書くのがトークンを送ってくる側だという点です。検証する側がalgを見て検証方法を選ぶなら、検証方法を選んでいるのは攻撃者です。

この記事は、JWTのベストプラクティス文書であるRFC 8725(BCP 225、2020年2月、RFC 7519を更新)と、その土台になるRFC 7515(JWS)、RFC 7518(JWA)、RFC 7519(JWT)に基づいています。扱うのはJWSで表現されたJWTで、署名なしのJWT(Unsecured JWT、alg: none)も含みます。暗号化されたJWT(JWE)は対象外です。2026年10月10日に確認したRFC EditorのRFC 8725情報ページには、この文書を廃止(Obsoleted by)または更新(Updated by)したRFCは表示されていません。

algはトークンの中にある

RFC 7515の4.1.1節で、algはJWSの保護に使った暗号アルゴリズムを示すヘッダーパラメーターであり、必ず含めなければなりません(MUST)。登録済みの値はRFC 7518の3.1節の表にあります。HMACのHS256・HS384・HS512、RSA署名のRS256・PS256系、ECDSAのES256系、そして署名なしを意味するnoneです。

ヘッダーはBase64URLでエンコードされたJSONにすぎないので、誰でも読めて書き換えられます(Base64の記事を参照)。JWSコンパクトシリアライゼーション(ドット2つで区切られたよく見る形)ではヘッダーが署名入力に含まれるため、ヘッダーを変えると元の署名は合わなくなります。RFC 7515の10.7節も、algを保護ヘッダーに置くことをアルゴリズム置換攻撃の緩和策の1つに挙げています。

ただしこの保護が意味を持つのは、検証者が自分で決めた方法で検証するときだけです。algを書き換えただけで、攻撃者が好きなアルゴリズムで有効な署名を作れるわけではありません。問題になるのは、攻撃者が選んだ方法を検証者が受け入れ、しかもその方法を攻撃者自身が満たせる場合です。noneや、既知のRSA公開鍵をHMACの秘密として使うHS256がその例です。このとき攻撃者はヘッダーを差し替え、欠陥のある検証者が受け入れる値を新たに作れます。

知られている2つの失敗

RFC 8725の2.1節は、algヘッダーがライブラリやアプリケーションの設計上の欠陥と組み合わさって攻撃につながった例を2つ挙げています。

1つ目はalgをnoneに変えるものです。一部のライブラリはこの値を信用し、署名を確認しないままJWTを「検証」していました。RFC 7519の6.1節にある署名なしJWTの例は次のとおりです(表示のために改行しています)。

eyJhbGciOiJub25lIn0
.
eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt
cGxlLmNvbS9pc19yb290Ijp0cnVlfQ
.

先頭は{"alg":"none"}、真ん中は{"iss":"joe","exp":1300819380,"http://example.com/is_root":true}(原文には改行が入っています)で、最後のドットの後ろの署名は空文字列です。鍵がまったく要らないので、誰でも作れます。

2つ目はRS256(RSA署名)をHS256(HMAC)に変えるものです。一部のライブラリは、RSA公開鍵をHMACの共有秘密として使ってHMAC-SHA256の検証を試みていました(RFC 8725が引用するCVE-2015-9235)。公開鍵は文字どおり公開されている値なので、攻撃者も同じバイト列でHMACを計算し、検証を通る署名を作れます。検証コードが「この鍵で検証して」とだけ指示し、アルゴリズムをヘッダー任せにしているときに起きる構造です。

仕様が求めていること

RFC 7515の5.2節は、JWSの検証手順の最後にこう書いています。どのアルゴリズムを使ってよいかはアプリケーションが決めることで、署名の検証に成功しても、そのアルゴリズムがアプリケーションにとって受け入れられるものでなければJWSは無効とみなすべきです(SHOULD)。RFC 7519の7.2節もJWTについて同じことを述べています。

RFC 8725はこれをより強く書いています。

  • ライブラリは、呼び出し側がサポートするアルゴリズムの集合を指定できるようにしなければならず、それ以外のアルゴリズムを使ってはなりません(3.1節、MUST)。
  • ライブラリは、algが実際に行う暗号処理のアルゴリズムと同じであることを確認しなければなりません(3.1節、MUST)。
  • 1つの鍵はちょうど1つのアルゴリズムにだけ使い、暗号処理の際にそれを確認しなければなりません(3.1節、MUST)。
  • アプリケーションは、自身のセキュリティ要件を満たす、現時点で安全なアルゴリズムだけを許可しなければなりません(3.2節、MUST)。

noneについての規則はRFC 7518の3.6節にあります。署名なしJWSをデフォルトで受け入れてはならず(MUST NOT)、ダウングレード攻撃を防ぐため、アプリケーション全体で「none許可」を有効にしてはなりません(MUST NOT)。許可が必要ならオブジェクトごとに示すべきです(SHOULD)。8.5節はその理由を例で示しています。HTTPで受け取るオブジェクトには署名を求め、クライアント認証付きのHTTPSで受け取るオブジェクトにはnoneを許すアプリケーションが、noneをグローバルに許可したとします。すると攻撃者がHTTPで署名なしJWSを送っても通ってしまいます。

もう1つ知っておきたい点があります。RFC 7519の8節は、仕様に準拠するJWT実装はHS256とnoneを必ず実装しなければならない(MUST)と定めています。ライブラリがnoneをそもそも知らないことは期待できません。止める場所は検証の呼び出しです。RFC 8725の3.2節も、呼び出し側が明示的に求めない限り、ライブラリはnoneのJWTを受け入れるべきではない(SHOULD NOT)としています。

なおRFC 8725の3.2節は、JWTがTLSのようなトランスポート層でエンドツーエンドに保護されている場合はnoneでも問題ないことがある、とも述べています。ただしその場合も、JWTがほかの手段で暗号的に保護されているときに限る、という条件付きです。

アルゴリズムを鍵に結びつける

これらの要件を満たす簡単な方法は、アルゴリズムをトークンではなく鍵の設定側に置くことです。以下はその構造を示す説明用の疑似コードで、特定のライブラリのAPIではありません。

// 説明用の疑似コードです。特定のライブラリのAPIではありません。
// kid → { この鍵に許可する唯一のアルゴリズム, 鍵 }
const trustedKeys = new Map([
  ["2026-10-a", { alg: "ES256", key: issuerPublicKeyA }],
]);

function verifyJwt(token) {
  const header = decodeProtectedHeader(token); // まだ何も信用していない値
  const entry = trustedKeys.get(header.kid);   // 信頼済みの一覧の中だけで探す
  if (!entry) return reject("unknown key");
  if (header.alg !== entry.alg) return reject("algorithm mismatch");
  if (!verifySignature(entry.alg, entry.key, token)) return reject("bad signature");
  // 署名が合ってから iss, aud, exp, nbf, typ を確認する
}

algがnoneでもHS256でも、鍵に記録した値と違えば署名の計算までたどり着きません。検証に使うアルゴリズムも、ヘッダーではなくentry.algから取っています。発行者が鍵を1つしか使わないなら、kidなしで固定の鍵1つとアルゴリズム1つで足ります。

鍵を選ぶのに使うヘッダーも、信頼できない入力です。

  • kidはどの鍵を使ったかのヒントで、構造は決まっていません(RFC 7515の4.1.4節)。データベースやLDAPの検索に使うなら、SQLやLDAPのインジェクションが起きないよう検証またはサニタイズする必要があります(RFC 8725の3.10節)。
  • jku(JWK Set URL)やx5u(X.509 URL)をそのままたどると、任意のURLにリクエストを送ることになり、SSRFにつながりえます。RFC 8725の3.10節は、許可リストと照合し、GETリクエストにCookieを付けない方法を例に挙げています(SHOULD)。
  • 公開鍵を取得する方法は、鍵の出所を認証するものでなければなりません(RFC 7515の10.3節)。トークンがjwkヘッダーで自分自身に添えてきた鍵は、ほかの根拠で発行者の鍵だと確認しない限り、出所が確認されていない鍵です。

HMACを使う場合は鍵そのものも問題になります。RFC 7518の3.2節は、ハッシュ出力以上の長さの鍵、つまりHS256なら256ビット以上を求めています(MUST)。RFC 8725の3.5節は、人が覚えられるパスワードをHS256のようなMACの鍵として直接使ってはならない(MUST NOT)と定めています。2.2節によれば、そうした弱い鍵は、攻撃者がトークンを1つ手に入れるだけでオフラインの総当たり攻撃や辞書攻撃の対象になります。計算したHMACと受け取った署名の比較は、タイミング攻撃を防ぐため定数時間で行わなければなりません(RFC 7518の3.2節、MUST)。

署名が合った後に確認すること

署名が合うというのは、この鍵を持つ誰かがこの内容に署名した、というところまでです。RFC 7519の11.1節は、JWTの内容は暗号的に保護され、信頼判断に必要な文脈に結びつけられていない限り信用できないと述べています。

  • iss:issがあるなら、署名に使われた鍵がその発行者のものか確認し、違えば拒否しなければなりません(RFC 8725の3.8節、MUST)。鍵と発行者の結びつけ方はアプリケーションごとに異なり、OpenID Connectでは発行者メタデータのjwks_uriから鍵を取得します。
  • sub:subがあるなら、その値がアプリケーションにとって有効な主体(または発行者と主体の組)か確認しなければなりません(3.8節、MUST)。
  • aud:1つの発行者が複数の受信者向けにJWTを発行するなら、JWTにはaudが必要で、受信者はその値がないか自分と関係ない場合に拒否しなければなりません(3.9節、MUST)。別のサービス向けに発行されたトークンを使い回す置換攻撃(2.7節)を防ぐ仕組みです。
  • exp / nbf:expの時刻以降とnbfの時刻より前は受け入れてはなりません(RFC 7519の4.1.4節・4.1.5節)。時計のずれに備えた余裕は、通常数分以内で設けてよいとされています(MAY)。
  • typ:同じ発行者が複数の種類のJWTを発行するなら、種類ごとの検証規則が互いを拒否するように書かなければなりません(3.12節、MUST)。新しく設計するJWTでは、typで種類を明示することが推奨されています(3.11節、RECOMMENDED)。たとえばセキュリティイベントトークンはsecevent+jwtを使います。

検証チェックリスト

  1. 許可するアルゴリズムを設定で固定し、ヘッダーのalgがその一覧になければ拒否する。
  2. 鍵ごとにアルゴリズムを1つだけ決め、algがその鍵のアルゴリズムと同じか確認する。
  3. noneはデフォルトで拒否する。必要な場合だけ、そのオブジェクトに限って許可する。
  4. 鍵は信頼できる設定か発行者メタデータからだけ取得する。kidはサニタイズし、jku・x5uは許可リストと照合する。
  5. HMACの鍵はランダムに生成した十分な長さの値を使う。HS256なら256ビット以上で、パスワードは使わない。
  6. 署名が合った後、issと鍵の関係、sub、aud、exp・nbf、必要ならtypを確認する。
  7. どの段階で失敗してもJWT全体を拒否する(RFC 7519の7.2節、RFC 8725の3.3節)。

これで防げないもの

アルゴリズムを固定すれば、トークンが自分の検証方法を選ぶ問題はなくなります。ただし次のものは残ります。

  • 有効なトークンの再利用:正規に発行されたトークンが漏れれば、署名は合い続けます。RFC 7515の10.10節は一意の識別子を入れて受信済みかどうか確認する方法に触れており、jtiはこの用途に使えます(RFC 7519の4.1.7節)。
  • 署名鍵の漏えい:秘密鍵やMACの鍵が漏れれば、攻撃者は正規の発行者と同じように署名できます(RFC 7515の10.2節)。
  • 内容の秘匿:署名は改ざんを防ぐだけで、ペイロードを隠しません。
  • JWEとネストしたJWT:暗号化されたJWTの復号後に署名検証を省いてしまう問題(RFC 8725の2.3節)や、ECDH-ESの入力検証(2.5節)はこの記事では扱っていません。

検証に失敗したときにどのステータスコードで応答するかは、HTTP 401と403の違いにまとめています。

参考にした文書

リンクと内容は2026年10月10日に確認しました。

ブログへ