メインコンテンツへスキップ
ブログ一覧に戻る
Web制作・運用

JWTは暗号化ではない|署名が守っているものを知るとペイロードの設計が変わる

2026年9月30日
12分で読めます
JWTは暗号化ではない|署名が守っているものを知るとペイロードの設計が変わる

この記事の結論

JWTを「署名済みだから安全なトークン」と考えて、ペイロードに社内情報を載せていませんか。RFC 7519とRFC 8725の記述に基づき、署名が守るのは改ざん検知であって中身の秘匿ではないことを整理します。何を載せるか・どのアルゴリズムを許すかという実装側に残る判断を、レビュー用の4つの問いに落とします。

JWTは暗号化ではない|署名が守っているものを知るとペイロードの設計が変わる

ログインやAPI(システム同士をつなぐ窓口)の認証にJWT(JSON Web Token)を使うとき、多くの実装者はどこかの時点でこう考えます。「署名が付いているから、改ざんもできないし中身も見られない」。そしてペイロードに、ユーザーIDに加えて社内の役職コードや契約プランまで載せていきます。

ここには一段の飛躍があります。署名が保証しているのは「途中で書き換えられていないこと」であって、「中身が見えないこと」ではありません。そして何を載せてよいか、受け取った値をどこまで疑うかは、仕様が決めてくれる部分ではなく、実装した人の判断が残っている部分です。

本記事は、この「署名済み」という言葉の読み替えが、ペイロードの設計とトークン検証の抜け穴の両方を生んでいるのではないか、という見方で整理します。扱うのは仕様が実装側に残した判断の範囲で、トークンをCookieに置くかブラウザの保存領域に置くかという保存場所の選択、および個別ライブラリの既定の挙動は扱いません。

30秒で要点

  • JWTのクレーム(トークンに載せる情報)は「署名(JWS)のペイロード」か「暗号化(JWE)の平文」として符号化される。署名と暗号化は別の選択肢である
  • 署名も暗号化もないJWTは仕様上存在する(Unsecured JWT、algがnone)。受け取る側が許可するアルゴリズムを決めていないと、これを通す経路が残る
  • 鍵は1つのアルゴリズムとともに使うこと、人が覚えられるパスワードをHS256などの鍵に直接使わないことが、RFC 8725で求められている
  • 受け取ったクレームは検証前の入力である。kidを検索に流用すればインジェクションの、jku・x5uのURLを盲目的にたどればSSRFの経路になり得る

なぜ「署名済み」が「中身は見えない」に読み替わるのか

この誤読が起きるのは、実装者が不注意だからではありません。言葉の並びに原因があります。

ひとつは、署名も暗号化も「暗号技術」という同じ箱に入っていることです。日常語の「暗号」は「読めなくすること」を指すため、暗号技術を使っていると聞いた時点で「読めなくなっている」という連想が働きます。実際には、署名は読めることを前提に「読めている内容が発行時のままか」を確かめる技術です。目的が正反対に近いのに、同じ棚に並んでいます。

もうひとつは、JWTの見た目です。ヘッダとペイロードはBase64urlという方式で符号化されており、画面上は意味の読めない英数字の並びになります。人は「読めない文字列」を見ると、それが「読めないように処理された文字列」だと解釈しがちです。しかし符号化は、扱いやすい文字の範囲に収めるための変換にすぎません。

「暗号技術を使っている」と「読めない文字列に見える」が重なると、確認せずに「秘匿されている」と判断する条件が揃います。ここを疑わないまま設計が進むと、ペイロードは少しずつ太り、後から「あの値は外に出ていた」と気づくことになります。

確かなこと・まだ確かではないこと

確かなこと:RFC 7519(2015年5月発行、JWTを定義する標準化文書)は冒頭で、JWTのクレームはJSONオブジェクトとして符号化され、それがJWS(JSON Web Signature)構造のペイロード、またはJWE(JSON Web Encryption)構造の平文として使われると述べています。そのうえで、これによりクレームをデジタル署名またはMAC(メッセージ認証符号)で完全性保護すること、および/または暗号化することが可能になるとしています。

この一文の構造が重要です。完全性保護と暗号化は「および/または」で並んでおり、どちらか一方だけを選ぶこともできます。署名だけを選んだJWTは、仕様の想定どおりの正常な使い方であり、同時に中身が読める状態です。

まだ確かではないこと:本記事が参照するRFC 8725(2020年2月発行、JWTのBCP 225としてRFC 7519を更新する文書)は、自らについて「セキュリティに関するBCP文書は、ある時点での表明である」と述べています。2020年時点での最善の実践であり、その後に環境が変わっている可能性は残ります。また、個々のライブラリが現在どの挙動を既定にしているかは、この2つの文書からは判断できません。

署名が守っているのは改ざん検知であって秘匿ではない

RFC 7519の定義を受け入れると、設計上の問いが入れ替わります。「このトークンは安全か」ではなく、「このトークンに載せた情報は、第三者に読まれても成立するか」が問いになります。

そして仕様は、署名も暗号化もない形も認めています。RFC 7519はUnsecured JWTという用語を「クレームが完全性保護も暗号化もされていないJWT」と定義し、それはalgヘッダの値がnoneで、署名の値が空文字列のJWSであると述べています。

つまり、「JWTである」ことは「署名で保護されている」ことを意味しません。何で保護されているかは、トークンのヘッダが主張しているだけです。そして主張は、受け取る側が確かめるまで主張のままです。

暗号化を選んだ場合にも判断は残ります。RFC 7519は、暗号化されたJWTで一部のクレームをヘッダに複製する場合、暗号化されていない形で送ってよいクレームだけを複製するのはアプリケーションの責任だと書いています。仕様が「ここはあなたの責任だ」と明示している箇所は、実装レビューで最初に見る場所です。

「検証した」と言えるのは3つが揃ったときだけ

JWTの実装を読んでいると、verify()が呼ばれていることを確認して「検証している」と判断してしまいがちです。しかしRFC 8725の指摘を踏まえると、検証という言葉は3つの要素に分解できます。この3つを別々に数えるのが、レビューの単位です。

1つめは、どのアルゴリズムを許すか。RFC 8725は、JWT仕様の公開以降、実装と運用に対する攻撃が広く報告されてきたとし、その原因を「仕様の不十分な点、不完全な実装、アプリケーションによる誤用」に求めています。具体例として挙げているのは2つです。攻撃者がalgをnoneに変え、それを信じたライブラリが署名を確認せずに「検証済み」として扱ってしまう経路。そしてRS256(RSA)をHS256(HMAC-SHA256)に変えられ、RSAの公開鍵をHMACの共有鍵として使って検証してしまう経路です。いずれも「トークンが名乗ったアルゴリズムを、受け取る側がそのまま採用した」ことが成立条件になっています。

2つめは、どの鍵を使うか。RFC 8725は、各鍵はちょうど1つのアルゴリズムとともに使わなければならず(MUST)、暗号処理の時点でそれを確認しなければならないとしています。またnoneは他の手段で暗号的に保護されている場合にのみ使うべきで、JWTライブラリは呼び出し側が明示的に要求しない限りnoneのJWTを生成も受理もすべきでない(SHOULD NOT)としています。鍵の質にも指針があります。人が覚えられるパスワードのようなエントロピー(推測しにくさ)の足りない鍵をMACに使うと、トークンを入手した攻撃者によるオフラインでの総当たり・辞書攻撃に脆弱になるため、そうしたパスワードをHS256のような鍵付きMACの鍵に直接使ってはならない(MUST NOT)と書かれています。

3つめは、受け取ったクレームをどう扱うか。RFC 7519は、登録済みクレーム(___none0___・___none1___・___none2___・___none3___・___none4___・___none5___・___none6___)の使用はいずれもOPTIONALだとしています。ただし___none7___が存在していて、処理する側が___none8___の値で自分を識別できない場合、そのJWTは拒否しなければならない(MUST)と定めています。発行側の任意と受信側の義務は別だということです。

署名検証が通ったことは、この3つのうち1つめと2つめの一部が成立したことしか示しません。3つ揃っていないなら、「検証した」と数えないほうが実態に合います。

受け取ったクレームは、信頼できる値ではなく入力である

RFC 8725には、実装者の直感から最も外れやすい指針があります。JWTに含まれる値そのものが攻撃の入口になり得る、という指摘です。

同文書は、これらのクレームがインジェクション攻撃やSSRF(サーバー側リクエストフォージェリ、サーバーに意図しない通信をさせる攻撃)の経路として使われ得るとしています。例として、___none9___(鍵の集合が置かれたURL)や___verify()0___(X.509証明書のURL)を盲目的にたどると任意のURLを含み得るためSSRFにつながる可能性があるとし、アプリケーションはURLの照合などで対策すべき(SHOULD)だと述べています。___verify()1___(鍵の識別子)をデータベース検索にそのまま使えば、検索に対するインジェクションの経路になります。

ここが「署名されているから安全」の最後の落とし穴です。ヘッダを読んで鍵を探す処理は、署名検証より先に動きます。検証前の値を検証済みの値と同じ扱いにしていないか、という観点でコードの順序を見る必要があります。

種類の取り違えにも指針があります。RFC 8725は、同じ発行者が複数種類のJWTを発行する場合、検証ルールを相互排他に書き、種類の違うJWTを拒否しなければならない(MUST)としています。明示的に型を示す手段として___verify()2___ヘッダの利用を挙げ、例としてSecurity Event Tokenが___verify()3___というメディアタイプを使っていることを示しています。アクセストークン用の検証関数がパスワードリセット用トークンも通してしまう事故は、実装ミスというより検証ルールを相互排他に書いていないことの帰結です。

判断軸:JWTの実装をレビューするときの4つの問い

コードを読む順番として、次の4つを分けて確認すると、見落としの位置が特定しやすくなります。

  1. ペイロードに載っている情報は、第三者に読まれても成立するか:署名だけを付けている(JWSの)JWTなら、載せた値は読める前提で考えます。読まれると困る情報があるなら、暗号化を選ぶか、その値を載せずにサーバー側で引く設計に変えます。
  2. 受け取る側が、許すアルゴリズムを自分で決めているか:トークンのヘッダが名乗った___verify()4___をそのまま採用していないかを見ます。___verify()5___を受理する経路が明示的な要求なしに開いていないかも確認します。
  3. 鍵は1つのアルゴリズムに固定されているか。その鍵は人が覚えられる文字列ではないか:署名用の鍵が複数の用途で使い回されていないか、___verify()6___の鍵が設定ファイル内の短いパスワードになっていないかを見ます。
  4. 署名検証の前に読んでいる値を、検証後の値と同じ扱いにしていないか:___verify()7___を検索に流用している箇所、___verify()8___や___verify()9___のURLをたどっている箇所を探します。トークンの種類が複数あるなら、検証ルールが相互排他かも合わせて見ます。

この4つは「仕様に従っているか」ではなく「仕様が実装側に残した判断を、実際に判断したか」を問う形です。

自分でも試せる、最小の検証

いま運用しているシステムが発行しているJWTを1つ取り出し、ヘッダとペイロード(Base64urlで符号化された部分)を復号して中身を表に書き出してください。そして各クレームの横に、「この値が第三者の画面に表示されても業務上問題ないか」を一言で書き込みます。

問題があると書いた行が1つでもあれば、そのシステムは「署名されているから見えない」という前提で設計されています。その行が最初に扱うべき箇所です。合わせて、受け取り側のコードで許可するアルゴリズムを明示している行を探してください。見つからない場合、トークンの主張をそのまま採用している可能性があります。

判断の土台として押さえておくこと

  • 署名と暗号化は別の選択肢であり、署名だけの状態は中身が読める状態である
  • 署名も暗号化もないJWT(___alg0___が___alg1___)は仕様上定義されている。受け取る側が許可するアルゴリズムを決めることが前提になっている
  • 鍵は1つのアルゴリズムに限定し、人が覚えられるパスワードを鍵付きMACの鍵に直接使わない
  • ___alg2___・___alg3___・___alg4___の扱いと、トークン種類ごとの相互排他な検証は、実装側の責任として残る

認証の実装方針を含め、システムの前提を洗い出して整理したい場合は、状況を整理するところから始めることができます。

状況を整理する(15分)

より深く学ぶ

参考資料・引用元

  • IETF, "RFC 7519: JSON Web Token (JWT)"(2015年5月). https://www.rfc-editor.org/rfc/rfc7519.txt
  • IETF, "RFC 8725: JSON Web Token Best Current Practices"(2020年2月、BCP 225). https://www.rfc-editor.org/rfc/rfc8725.txt

よくある質問(FAQ)