IETFOAuth Working Groupは、アイデンティティ分野における標準の作成と改良に熱心に取り組んでいます。この記事では JSON Web Token (JWT) の最新ベスト プラクティスについて書かれた直近のドラフトについて取り上げます。対象のドラフトでは、JWT の使用に際して陥りがちな落とし穴や、よく見られる攻撃方法に加えて、そうした問題に対する軽減策の実施方法を紹介していますので、ぜひご一読ください。

"JWT を標的とする特に一般的な攻撃方法と、具体的な保護対策が紹介されています"

はじめに

JSON Web Token (JWT) 仕様は、2 者間でのクレーム (属性情報) の伝送を目的とした、JSON ベースの形式について規定したオープン標準 (RFC 7519)です。 JWT を補完する標準として、JSON Web Key (RFC 7517), JSON Web Signature (RFC 7515), JSON Web Encryption (RFC 7516), and JSON Web Algorithms (RFC 7518) などがあり、検証機能や暗号化機能で JWT を拡張することができます。

JWT には実に多くの使い道があります。認証と認可 に関するクレームを 2 者間で伝送するのもその 1 つです。たとえば、 ID プロバイダーによって認証されたユーザーは、一連の署名付き JWT クレームの保持を許可され、アプリケーションに対する本人確認に使用することができます。

アイデンティティの分野における JWT は、 OAuth2 フレームワークをベースとする仕様である OpenID Connect 標準の一部として、きめ細かい認証レイヤーの提供に欠かせない存在となっています。

もっとわかりやすい JWT の実例としては、ブラウザーやモバイル クライアント上でクレームを保管するために使用される、暗号化や署名を施されたトークンが挙げられます。こうしたクレームは受信者側で、共有シークレットまたは公開キーを使用して簡単に検証できます。

JWT 仕様では独自に作成したプライベート クレームがサポートされ、検証済みまたは暗号化された任意のデータを、JSON 形式に簡単にエンコードしてやり取りできるので便利です。

JWT の概要、業界での使用例、アルゴリズムとライブラリの詳しい実装方法などにご興味をお持ちでしたら、以下のリンクから無料の JWT ハンドブックをご覧ください。

あらゆるツールがそうであるように、JWT にも JWT ならではの落とし穴があり、JWT を標的とした攻撃も行われています。 JSON Web Token の最新ベストプラクティスは、そうした落とし穴や攻撃について紹介しながら、防止策をわかりやすく解説したドキュメントです。今回の記事では、このドキュメントに挙げられた攻撃と落とし穴について確認した後、軽減策とベスト プラクティスを見ていくことにします。

落とし穴と攻撃

1 つ目の攻撃について紹介する前に、注意していただきたい点があります。それは、JWT を標的とする攻撃の多くは、JWT の仕様設計というよりも、その実装方法に関連しているということです。だからと言って、攻撃への警戒を緩めてよいわけではありません。基盤となる設計を変更することが、こうした攻撃の軽減策として有効かどうかについては議論の余地があります。当分の間、JWT の仕様や形式が変わることはありません。そのため、ほとんどの変更は実装レベル (ライブラリ、API、プログラミングの手法や規則の変更) で行われます。

もう 1 つ重要な点として、JWT で最も一般的に使用される表現形式、 JWS Compact Serialization の基本概念を理解しておきましょう。シリアル化を解除すると、JWT は主に headerpayload という 2 つの JSON オブジェクトで構成されています。

ヘッダー オブジェクトには JWT 自体の情報が含まれます。具体的には、トークンのタイプ、使用されている署名または暗号化のアルゴリズム、キー ID などです。

一方、ペイロード オブジェクトには、そのトークンによって運ばれるすべての関連情報が含まれます。このオブジェクトには、sub (subject の略。ユーザーの識別子を表す) や iat (issued at の略。発行時間を表す) などの標準のクレームに加え、任意のカスタム クレームを含めることもできます。

こうしたオブジェクトは JWS Compact Serialization 形式を使用してエンコードされ、次の例のように変換されます。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.XbPfbIHMI6arZ3Y922BhjWgQzWXcXNrz0ogtVhfEd2o

上記は署名付きの JWT です。署名付きの JWT をコンパクト形式にすると、Base64-URL エンコードされたヘッダー オブジェクトとペイロード オブジェクトがドット (.) で区切って連結されます。署名は、このコンパクトな表現の最後の部分に当たります。具体的には、次のような形式になります。

[Base64-URL encoded header].[Base64-URL encoded payload].[Signature]

上記は署名付きトークンのみに適用される形式です。暗号化されたトークンは別のシリアル化されたコンパクト形式で表現され、同じく Base64-URL エンコーディングとドットで区切ったフィールドが使用されます。

「JWT を触ってみたい」「エンコードとデコードのしくみを知りたい」という方は、JWT.io を参照してください。

"alg": "none" 攻撃

先ほども述べたように、JWT ではヘッダーとペイロードという、重要な情報を含む 2 つの JSON オブジェクトを伝送します。このヘッダーには、JWT 内のデータの署名や暗号化に使用されるアルゴリズムに関する情報が含まれます。署名付き JWT の署名はヘッダーとペイロードの両方に適用されますが、暗号化された JWT ではペイロードのみが暗号化されます (ヘッダーは常に判読可能でなくてはならないため)。

署名付きトークンの場合、ヘッダーとペイロードの改ざんは署名によって防止できますが、署名を使用せずに JWT を書き換えて、内部のデータを変更することは可能です。これはどういうしくみなのでしょうか?

次のようなヘッダーとペイロードを含む JWT の例で考えてみましょう。

header: {
  alg: "HS256",
  typ: "JWT"
},
payload: {
  sub: "joe"
  role: "user"
}

このトークンに署名を付け、シリアル化されたコンパクト形式にエンコードするとします。署名キーは「secret」です。エンコードは JWT.io で実行できます。結果は次のとおりです。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9.vqf3WzGLAxHW-X7UP-co3bU_lSUdVjF2MKtLtSU1kzU

上記をコピーし、 JWT.io に貼り付けて確かめてみてください。

さて、これは署名付きトークンなので、だれでも判読できます。つまり、中身のデータを少しだけ変更して類似のトークンを作成できます。ただし、署名キーを知らなければ署名を付けることはできません。署名キーがわからない場合、攻撃者はどうするでしょうか? 悪意あるユーザーは、署名なしのトークンを使用して攻撃をかけてくることがあります。そのしくみを詳しく説明しましょう。

攻撃者はまず、トークンを改ざんします。たとえば、次のように変更したとします。

header: {
  alg: "none",
  typ: "JWT"
},
payload: {
  sub: "joe"
  role: "admin"
}

上記をエンコードします。

eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJqb2UiLCJyb2xlIjoiYWRtaW4ifQ.

このトークンには署名が含まれません ("alg": "none" によってアルゴリズムが "なし" に指定されているため)。さらに、ペイロードのロール (role) クレームが変更されています。攻撃者がこのトークンの悪用に成功すれば、特権エスカレーション攻撃をかけることができます。なぜこうした攻撃が有効なのか、架空の JWT ライブラリの例で考えてみましょう。次のようなデコード関数があるとします。

function jwtDecode(token, secret) {
  // (...)
}

この関数は、エンコードされたトークンとシークレットを使用し、トークンの検証を試行した後、トークン内のデータをデコードして返します。検証に失敗した場合は例外がスローされます。適切な検証アルゴリズムを選択するために、この関数はヘッダー内の alg クレームを参照します。これこそが、この攻撃が成功する要因です。従来多くのライブラリでは、このクレームを参照することで検証アルゴリズムを選択していました。そして皆さんももう予想がついているかもしれませんが、先ほど挙げた悪意あるトークンでは alg クレームが none になっています。つまり、検証アルゴリズムが設定されていないため、検証ステップが常に成功してしまうのです。

おわかりのように、これは昔からある攻撃のパターンで、特定のライブラリに含まれる API の特定のあいまい性を悪用したものであって、JWT の仕様自体の脆弱性ではありません。ただし、これは過去に複数の異なる実装において現実に行われた攻撃です。そのため、現在では多くのライブラリで、署名の有無にかかわらず、"alg": "none" という指定のトークンが無効にされています。このタイプの攻撃には他にも軽減策があり、そのうち最も重要なのが、トークンの検証を行う前に必ずヘッダー内のアルゴリズムの指定をチェックすることです。また、alg クレームを参照するのではなく、検証関数への入力値として検証アルゴリズムの指定を必要とするようなライブラリを使用する方法もあります。

RS256 公開キーを HS256 シークレットとして使用する攻撃

これは "alg": "none" と同じく、特定の JWT ライブラリに含まれる API のあいまい性を悪用するものです。トークンの例には、先ほどの "alg":" none" の場合と同様のものを使用します。ただし、こちらのケースでは署名を削除しません。代わりに、多くの API に含まれる欠陥を悪用することで、有効な署名を作成して、検証ライブラリの側でも有効だと判断されるようにします。最初に、一部の JWT ライブラリに含まれ、検証関数に使用される標準的な関数シグネチャを見てみましょう。

function jwtDecode(token, secretOrPublicKey) {
  // (...)
}

ご覧になってわかるように、この関数は "alg": "none" 攻撃の場合と基本的にはまったく同一です。検証に成功すればデコードされたトークンが返され、失敗すれば例外がスローされます。ただし、この例の関数の場合は、2 つ目のパラメーターとして、公開キーを受け入れる点が異なっています。これはある意味、理にかなった方法です。公開キーと共有シークレットには通常、どちらも文字列またはバイト配列が使用されます。そのため関数の引数に求められる型の観点では、単一の引数によって公開キーと (RS、ES、PS アルゴリズム向け) と共有シークレット (HS アルゴリズム向け) の両方を表すことができます。このタイプの関数シグネチャは、多くの JWT ライブラリで一般的に使用されています。

ここで、RSA キーのペアを使用して署名済みのエンコードされたトークンを攻撃者が入手したとしましょう。これは次のようなトークンです。

eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqb2UiLCJyb2xlIjoidXNlciJ9.QDjcv11Kcb69THVLKMErYqzy9htWlCDtBdonVR5SX4geZa_R8StjwUuuskveUsdJVgjgXwMso7pguAJZzoE9LEr9XCxau7SF1ddws4ONiqxSVXZbO0pSgbKm3FpkVz4Jyy4oNTs-bIYyE0xf8snFlT1MbBWcG5psnuG04IEle4s

デコードすると次のようになります。

header: {
  "alg": "RS256",
  "typ": "JWT"
},
payload: {
  "sub": "joe",
  "role": "user"
}

このトークンは RSA キーのペアを使用して署名されています。RSA の署名は秘密キーを使用して行われますが、検証の実行には公開キーが使用されます。つまり、このトークンの検証を行おうとするユーザーはだれでも、次の要領で先ほどの架空の jwtDecode 関数を呼び出せます。

const publicKey = '...';
const decoded = jwtDecode(token, publicKey);

しかし、ここで問題が発生します。公開キーはその名のとおり、公開されているのが一般的です。当然ながら攻撃者も手に入れることができますが、それ自体は問題ないでしょう。ただし、これから説明する方法で攻撃者が新しいトークンを作成した場合は話が変わります。攻撃者はまずヘッダーに変更を加え、署名アルゴリズムに HS256 を指定します。

header: {
  "alg": "HS256",
  "typ": "JWT"
}

次に、ペイロード内の role クレームを変更してアクセス許可を昇格させます。

payload: {
  "sub": "joe",
  "role": "admin"
}

ここからが攻撃のポイントです。攻撃者は入手した公開キーを使用して、エンコードされた JWT を新たに作成しますが、この公開キーと HS256 の共有シークレットは、どちらもシンプルな文字列なのです。言い換えれば、HS256 の共有シークレットにはどんな文字列でも使用でき、もちろん RS256 アルゴリズムの公開キーでもかまいません。

ここで、先ほどの架空の jwtDecode 関数の例に戻ります

const publicKey = '...';
const decoded = jwtDecode(token, publicKey);

何が問題かはもうおわかりでしょう。これでトークンが有効だと見なされてしまうのです。公開キーが 2 番目の引数として jwtDecode 関数に渡され、RS256 アルゴリズムの公開キーとしてではなく、HS256 アルゴリズムの共有シークレットとして使用されます。この問題は JWT の検証アルゴリズムを選択する際、jwtDecode 関数がヘッダー内の alg クレームを参照しているために起こります。攻撃者はそのクレームを次のように変更しました。

header: {
  "alg": "HS256", // <-- changed by the attacker from RS256
  "typ": "JWT"
}

前述の "alg": "none" と同様に、欠陥のある、または紛らわしい API と alg クレームの参照を組み合わせることで、悪意あるユーザーによる攻撃が成功してしまいかねません。

この攻撃に対する軽減策としては、jwtDecode 関数にアルゴリズムを明示的に引き渡す方法や、alg クレームをチェックする方法、公開キー アルゴリズムと共有シークレット アルゴリズムを切り離すような API を使用する方法などが挙げられます。

脆弱な HMAC キー

HMAC アルゴリズムでは署名の作成と検証に、共有シークレットを使用しています。共有シークレットをパスワードと同じようなものと思っている方もいらっしゃるでしょう。関係者以外には秘密にしておく必要があるという点では、その認識も間違いではありません。しかし、両者の間にそれ以上の共通点はないのです。パスワードは長さが重要な特性になりますが、他のタイプのシークレットと比べると、長さの最小要件は相対的に短いと言えます。これは、パスワードの保存に使用されるハッシュ アルゴリズム (およびソルト) の影響で、相応の時間、ブルート フォース攻撃を防ぐことができるためです。

それに対し、JWT で使用されるような HMAC の共有シークレットはスピード重視で設計されています。結果として、多くの署名・検証処理を効率的に実行できる一方でブルート フォース攻撃を受けやすくなります。そのため、HS256 / 384 / 512 の共有シークレットの長さには最大限の注意が必要です。具体的に言うと、 JSON Web Algorithms では、キーの長さは最低限、HMAC アルゴリズムと併用するハッシュ関数と同じビット数以上のサイズでなくてはならないと定義されています。

"このアルゴリズムと併用するキーは、ハッシュ出力 ("HS256" なら 256 ビットなど) と同サイズ、またはそれ以上である必要があります" - JSON Web Algorithms (RFC 7518), 3.2 HMAC with SHA-2 Functions (SHA-2 関数を使用した HMAC)

別の言い方をすれば、他のコンテキストなら使用できるパスワードの多くが、HMAC で署名された JWT と使用する場合には、必要な条件を満たさないことになります。256 ビットは ASCII 文字 32 個に相当するため、人が読んで意味を成すようなパスワードにするなら、少なくともこの数以上の文字数をシークレット内に含めるようにしましょう。もう 1 つの選択肢として、RS256 など、より強固かつ柔軟性の高い他の公開キー アルゴリズムに切り替える方法も効果的です。これは単なる仮定的な攻撃ではありません。別の記事でも示していますが、共有シークレットが短すぎる場合、HS256 へのブルート フォース攻撃はごく簡単に実行できます。

暗号化および署名の検証の前提データに関するスタックのミス

署名は改ざんを防ぐ役割を果たします。つまり、署名にデータの解読を防ぐ力はありませんが、データが変更されたときに署名が無効となることで、変更の防止に効果を発揮します。一方、暗号化を使用すると、共有キーや公開キーを知らない限り、データを解読することができません。

多くのアプリケーションでは、署名機能を使用するだけで十分ですが、機密のデータについては暗号化が必要となる場合があります。JWT では、署名と暗号化の両方がサポートされます。

よくある思い違いが、暗号化さえすれば改ざんを必ず防げるというものです。多くの方が「データを解読できないなら、攻撃者が改ざんして悪用することもできないだろう」と考えているのでしょう。しかし残念ながら、攻撃者への認識が甘く、攻撃プロセスにかかわるアルゴリズムについて攻撃者の持つ知識を過小評価していると言わざるを得ません。

一部の暗号化・復号アルゴリズムは、渡されたデータの有効性には関係なく、出力処理を実行します。すなわち、たとえ暗号化されたデータが改ざんされていたとしても、何かしらのデータが復号プロセスによって出力されるということです。何も考えずに変更されたデータなら、通常は出力結果も意味のないものでしかありませんが、悪意ある攻撃者にとっては、システムに侵入するのに十分な手段となり得ます。たとえば、次のような JWT ペイロードがあるとしましょう。

{
  "sub": "joe",
  "admin": false
}

ご覧になってわかるとおり、admin クレームはシンプルなブール値で表されます。もしも攻撃者が復号データに変更を加える方法を見つけ出し、このブール値を「true」にすることができたなら、特権エスカレーション攻撃に成功してしまう可能性があります。具体的に言えば、攻撃にたっぷりと時間をかけられるならば、攻撃者は暗号化されたデータを改ざんし放題になります。トークンがシステムによって無効と判断され、処理前に破棄されることもありません。その他の攻撃として、既にサニタイズされたデータが送られてくることを前提としているサブシステムに対し、無効なデータを送り込むことにより、バグや障害を発生させたり、別のタイプの攻撃の侵入経路として利用したりする方法もあります。

こうした理由から、 JSON Web Algorithms の定義には、データ整合性の検証機能を備えた暗号化アルゴリズムしか含まれていません。ということは、使用された暗号化アルゴリズムが JSON Web Algorithms で承認されたアルゴリズムのいずれかである限り、そのアプリケーションに関して、署名付きの JWT の上に暗号化された JWT をさらに重ねる必要は必ずしもありません。ただし、標準以外のアルゴリズムを使用して JWT を暗号化する場合には、そのアルゴリズムによってデータの整合性が確保されることを確認するか、JWT を入れ子にし、最も内側にくる JWT に署名を付けることで、データの整合性を確保する必要があります。

入れ子にされた JWT は、仕様内で明示的にサポート対象として定義されています。一般的ではないものの、他のシナリオで使用されるケースもあり、同じく JWT を使用するサード パーティ システムを通じて、第三者が発行したトークンを伝送する場合などが考えられます

こうしたシナリオでは、入れ子にされた JWT の検証に関するミスが多く発生します。確実にデータの整合性を維持し、データが適切に復号されるようにするには、JWT を構成するすべてのレイヤーで、ヘッダー内に定義されている、アルゴリズムに関連したすべての検証を渡す必要があります。つまり、入れ子の一番外側にある JWT の検証と復号に成功したとしても、内側の JWT のすべてで検証 (および復号) を実行しなければなりません。これを怠ると、入れ子の一番外側に暗号化された JWT があり、一番内側に署名付き JWT がある場合は特に、未検証のままでデータを使用し、関連するさまざまなセキュリティ問題を引き起こしてしまう可能性があります。

Validation of Nested JWT

Invalid Curve 攻撃

楕円曲線暗号は、JSON Web Algorithms でサポートされる公開キー アルゴリズムの 1 つです。楕円曲線暗号は、数が一定以上の大きさになると現実的な時間で答えを求められない、楕円曲線上の離散対数問題という数学問題の難解さを安全性の根拠とする暗号化方式です。この数学問題が、公開キーや暗号化されたメッセージ、元のプレーンテキストから、秘密キーが割り出されるのを防いでいます。同じく JSON Web Algorithms でサポートされる公開キー アルゴリズムである RSA と比べて、楕円曲線暗号は同じ強度の暗号をより短いキーで実現できます。

暗号の演算に必要となる楕円曲線は、有限体上で定義されます。(すべての実数ではなく) 一連の離散数に関して演算を行うと言い換えることも可能です。これは、暗号の楕円曲線の演算に関するすべての数値が整数であることを意味します。

楕円曲線の数学的演算の結果はすべて、曲線上の有効な点として求められます。別の言い方をすれば、定義上、楕円曲線の演算結果に無効な点は絶対に存在しません。もしも無効な点が算出された場合は、演算の入力値が誤っていることになります。楕円曲線暗号に関する算術演算は、主に以下で構成されます。

  • 点の加算: 同一曲線上の 2 つの点を加算することで、同じ曲線上の 3 番目の点を算出します。
  • 点の 2 倍算: ある点に同じ点を加算することで、同一曲線上の新たな点を算出します。
  • スカラー倍算: 曲線上の 1 つの点にスカラー値を乗算します。これは、ある点に同じ点を k 回繰り返して加算すると定義できます (k はスカラー値)。

楕円曲線暗号の暗号処理の演算にはすべて、前述の算術演算が使用されます。ただし、一部の実装方法では入力値の検証が行われません。楕円曲線暗号では、公開キーが楕円曲線上の点を表すのに対し、秘密キーはシンプルな数値で、専用のきわめて広い範囲内から作成されます。演算への入力値が不正な場合、この算術演算では一見有効に見えて、実は無効な値が算出されることがあります。該当する演算結果を復号などの暗号処理のコンテキストで使用すると、秘密キーを復元されてしまいかねません。この攻撃は過去に実際に行われたことのあるものです。こういった攻撃は Invalid Curve 攻撃と呼ばれています。実装の安全性を強化するには、公開関数に渡されるすべての入力値について有効性を常にチェックしましょう。具体的には、公開キーが、選択した楕円曲線上にある有効な点になっていることや、秘密キーが有効な値の範囲内であることを確認します。

代替攻撃

代替攻撃とは、少なくとも 2 種類のトークンを攻撃者が傍受するような攻撃を指します。攻撃者は取得したトークンの 1 つまたは両方を本来の用途とは違う目的のために悪用します。

代替攻撃には 2 つのタイプがあり、1 つは同一受信者への攻撃 (ドラフトではクロス JWT と呼んでいる)、もう 1 つは異なる受信者への攻撃です。

異なる受信者

異なる受信者への攻撃では、ある受信者に対して発行されたトークンを別の受信者に送ることで攻撃が可能になります。仮に、サード パーティのサービスへのトークンを発行する認可サーバーがあるとしましょう。この認可トークンは、次のペイロードを含む署名入りの JWT です。

{
  "sub": "joe",
  "role": "admin"
}

このトークンを API に対して使用することで、認証済みユーザーとしてサービスにアクセスできるようになります。さらに、少なくともこのサービスに関し、ユーザーの joe に管理者レベルの特権が与えられます。しかし、このトークンには問題があります。それは、発行者はおろか、発行先となる受信者も指定されていない点です。このトークンの発行対象である本来の受信者以外の API で、有効性のチェック方法として署名のみが使用されている場合を考えてみましょう。前述の他のサービス (API) のデータベース内にも joe というユーザーがいるとします。この場合、そのサービスに先ほどのトークンを送信すれば、攻撃者は即座に管理者特権を入手できてしまうのです。

Different Recipient JWT Substitution Attack

このような攻撃を防ぐには、サービスごとに固有のキーやシークレットを使用するか、具体的なクレームを指定してトークンの検証を行う必要があります。たとえば、トークン内に aud クレームを追加し、対象者を指定するのも一案です。こうしておけば、たとえ署名が有効でも、同じシークレットや署名キーを共有する他のサービスでトークンを使い回すことはできなくなります。

同一の受信者 (クロス JWT)

この攻撃は前述の攻撃と似ていますが、今度は異なる受信者向けに発行されたトークンではなく、同じ受信者向けのトークンを利用します。前の例と違うのは、本来の発行対象とは異なる (同一社内または同一サービス プロバイダー内の) サービスに対して攻撃者がトークンを送る点です。

次のようなペイロードのトークンがあると考えてください。

{
  "sub": "joe",
  "perms": "write",
  "aud": "cool-company/user-database",
  "iss": "cool-company"
}

前の例と比べると、はるかにセキュアなトークンで、発行者 (iss) クレーム、対象者 (aud) クレーム、アクセス許可 (perm) クレームが含まれています。このトークンの発行対象である API は、たとえトークンの署名が有効でも、指定されたすべてのクレームをチェックします。そのため、攻撃者がどうにかして同じ秘密キーまたはシークレットで署名されたトークンを手に入れても、そのトークンを使用し、本来の発行対象以外のサービスを操作することはできません。

しかし、対象者の cool-company が他のパブリック サービスを提供しているとします。その 1 つに cool-company/item-database サービスがあり、トークンの署名に加えて、クレームをチェックするように最近アップグレードされたばかりだとしましょう。ところが、このアップグレードの最中に、検証対象のクレームを選択する担当チームが aud クレームの検証方法の判断を誤ってしまいました。完全一致でチェックすべきところを、cool-company という文字列を含む部分一致で検証するよう指定したのです。その結果、cool-company/user-database という別の (架空の) サービス向けのトークンでも、前述のチェック要件を満たせるようになります。つまり、攻撃者が user-database サービス向けのトークンを使用して、item-database サービスにアクセスできるようになってしまいました。そのため攻撃者は、本当は user-database に対する書き込みアクセス許可しか持っていないのに、item-database サービスへの書き込みアクセス許可を得られるのです。

Same Recipient JWT Substitution Attack

軽減策とベスト プラクティス

さて、ここまでは JWT を使用した一般的な攻撃方法について見てきましたが、続いては各種の最新ベスト プラクティスを紹介していきましょう。これまで挙げた攻撃はすべて、以降の推奨事項に従って防止できます。

常にアルゴリズムの検証を実行する

"alg": "none" 攻撃、および RS256 公開キーを HS256 シークレットとして使用する攻撃は、この軽減策によって防止できます。攻撃者に翻弄されないようにするためには、JWT の検証を行うたびに、毎回明示的にアルゴリズムを選択する必要があります。従来、多くのライブラリでは、ヘッダー内の alg クレームを参照することで検証用のアルゴリズムを選択していました。前述のような攻撃が一般的に見られるようになって以来、ヘッダー内の記載にかかわらず、少なくとも選択した検証用アルゴリズムを明示的に指定できるように各ライブラリが切り替わっています。しかし、一部のライブラリでは依然としてヘッダー内の任意の指定を使用できるようになっているため、常に明示的に選択したアルゴリズムを使用するよう、開発者が注意する必要があります。

適切なアルゴリズムを使用する

JSON Web Algorithms 仕様では一連の推奨アルゴリズムや必須アルゴリズムが定義されていますが、特定のシナリオに最適なものを選択するのはやはりユーザーの責任です。たとえば、ユーザーのブラウザーで使用する単一サーバーによるシングルページ Web アプリケーションの小規模トークンであれば、HMAC 署名付きの JWT で十分格納できるかもしれません。反対に、ID フェデレーションの実装シナリオで、共有シークレット アルゴリズムを適用する場合にはきわめて不都合でしょう。

別の考え方として、対象の検証アルゴリズムがアプリケーションにとって許容できるものでない限り、すべての JWT は無効であると言うこともできます。言い換えるなら、トークンの検証を実行するためにキーと必要な手段を揃えていたとしても、検証アルゴリズムが目的のアプリケーションに適切なものでなければ、検証は無効だと言わざるを得ません。このことは、先ほど述べた「常にアルゴリズムの検証を実行する」という推奨事項にも通じています。

常にすべての検証を実行する

複数のトークンを入れ子にしている場合は、各トークンのヘッダーで宣言された検証ステップを必ずすべて実行する必要があります。さらに言うなら、一番外側のトークンについて検証や復号を行い、内側のトークンの検証を省略するのでは不十分です。入れ子の内側にあるのが署名付き JWT だけだとしても、すべての署名を検証しなくてはなりません。これは、JWT を使用し、外部の組織で発行された他の JWT を伝送するアプリケーションでよく見られるミスです。

常に暗号処理の入力値を検証する

各攻撃に関するセクションで既に示したとおり、暗号関連の一部の演算では、その暗号の仕様から外れる入力値の処理方法が適切に定義されていないことがあります。こうした無効な入力値が悪用されれば、予期せぬ演算結果が算出されたり、機密情報が抜き取られたりして、深刻な情報侵害 (秘密キーが攻撃者の手に渡ること) につながりかねません。

前述の例で挙げた楕円曲線暗号の場合なら、公開キーを使用する前に必ずキーの有効性をライブラリで検証 (つまり、選択された曲線上の有効な点を表していることを確認) しましょう。この種のチェックは通常、基盤となる暗号化ライブラリによって処理されます。開発者は、選択したライブラリでこうした検証が実行されることを確認するか、検証の実行に必要なコードをアプリケーション レベルで追加する必要があります。これを怠ると、秘密キーの流出につながりかねません。

強力なキーを選択する

これはすべての暗号化キーに当てはまる推奨事項ですが、いまだに無視されている場面が多いのが現状です。既に示したように、HMAC の共有シークレットの長さに関する最小要件は見過ごされていることが少なくありません。ただし、共有シークレットは長さの要件を満たすだけでは不十分で、完全にランダムである必要があります。ランダム性 (すなわち「エントロピー」) が十分でない長いキーは、依然としてブルート フォース攻撃に弱く、キーを推測されるおそれがあります。これを確実に避けるために、キー生成ライブラリでは、初期化時に適切にシードされた暗号論的擬似乱数生成器 (PRNG) を使用するようにします。ハードウェア乱数生成器を使用しておけばよいでしょう。

この推奨事項は、共有キー アルゴリズムと公開キー アルゴリズムの両方に当てはまります。さらに、共有キー アルゴリズムの場合、人が読んで意味を成すようなパスワードは適切とは見なされず、辞書攻撃に対して弱いという特徴があります。

すべてのクレームを残さず検証する

ここで紹介した攻撃の一部は、検証の前提条件の誤りを悪用したものです。具体的な問題点は、署名の検証や復号を検証の唯一の手段としているところにあります。一部の攻撃者は正しく署名または暗号化されたトークンを手に入れ、通常はそれを想定外のコンテキストで用いることにより、トークンを悪意ある目的に利用します。こうした攻撃を防ぐための最善策は、署名とコンテンツの両方が有効である場合にのみ、トークンを有効であると見なすことです。そのため、sub (ユーザーの識別子)、exp (有効期限)、iat (発行時間)、aud (対象者)、iss (発行者)、nbf (有効になる日時) といったクレームが特に重要な意味を持ち、指定されている場合は常に検証を行わなくてはなりません。トークンを作成する際は、異なるコンテキストで利用されることがないよう、十分な数のクレームを追加しておくようにしましょう。一般的には、subissaudexp の各クレームは常に役に立つため、トークンに含めておくことをお勧めします。

typ クレームを使用してトークンのタイプを区別する

ほとんどの場合、typ クレームは 1 つの値 (JWT) しかとりませんが、アプリケーションに固有の各種 JWT を区別するために、このクレームを使用することもできます。この方法は、多くの種類のトークンをシステムで処理しなければならない場合に役立ちます。このクレームによって追加のクレーム チェックを行うことで、異なるコンテキストでのトークンの誤用を防ぐ効果もあります。JWS 標準には、typ クレームでアプリケーション固有の値を指定できることが明示されています。

トークンごとに異なる検証ルールを使用する

これは、これまで列挙してきたベスト プラクティスの多くを総括したものです。さまざまな攻撃を防ぐには、発行済みのすべてのトークンにきわめて明快かつ具体的な検証ルールを使用していることが特に重要になります。これには、typ クレームを適宜使用することや、issaud といった各種クレームを残さず検証することも大切ですが、それと同時に、異なるトークン間ではできるだけキーを使い回さないようにしたり、異なるカスタム クレームまたはクレーム形式を使用したりしなければなりません。こうすることで、単一の目的で使用するためのトークンを要件のきわめてよく似た別のトークンで代替できないようになります。

つまり、あらゆる種類のトークンに同じ秘密キーを使用して署名するのではなく、アーキテクチャのサブシステムごとに別の秘密キーを使用するようにしましょう。また、所定の内部書式を指定することで、クレームをより具体的にすることも可能です。たとえば、iss クレームの場合なら、発行者として会社名を指定する代わりに、そのトークンを発行したサブシステムの URL を指定することで、トークンの再利用を難しくすることができます。

補足:専門家に JWT 実装を委任する

JWT は OAuth2 フレームワーク上に存在するアイデンティティレイヤーであり、OpenID Connect スタンダード には不可欠な要素です。Auth0 は OpenID Connect に認定されたアイデンティティプラットフォームです。これはもしあなたが Auth0 を選んだ場合、同様に仕様に準拠したサードパーティシステムと 100% 相互運用可能であることを意味します。

OpenID Connect の仕様は、ID トークンに JWT フォーマットを使用すること求めています。ID トークンには、クレーム形式で表されるユーザーネームやパスワードのようなユーザープロファイル情報が含まれます。これらのクレームはユーザーに関するステートメントであり、トークン保持者が署名を検証できる場合、信用があるものとして扱われます。

OAuth2 の仕様では、ユーザーの代理でアプリケーションに API へのアクセスを許可するために使用されるアクセストークンのフォーマットは指定されていません。業界はアクセストークンに対する JWT の使用も広く受け入れています。

開発者として、サービス内で認証関連の JWT の直接検証や解読を心配すべきではありません。Auth0 が提供しているモダンな SDKs を使用することで、JWT の適切な実装と使用を行うことができます。JWT は最新のベストプラクティスに従っており、既知のセキュリティリスクに対処するために定期的に更新されています。

例えば、シングルページアプリケーションの Auth0 SDK は、 ID トークンからユーザー情報を抽出する方法 auth0.getUser を提供しています。

Auth0 プラットフォームを試してみたい場合は、フリーアカウントにサインアップして早速始めてみましょう!フリーアカウントでは、以下の機能をお使いいただけます:

JTW 、その内部構造、JWT で使用可能なアルゴリズムの種類、その他一般的な使い方などに関してより詳細を知りたい場合は、 JWT ハンドブックをご覧ください。

まとめ

JSON Web Token は暗号を活用したツールです。同種のさまざまなツール、特に機密情報を扱うものと同じく、JWT の使用には注意を要します。一見するとシンプルなため、共有シークレットや公開キーのアルゴリズムを正しく選択しさえすれば JWT を簡単に使用できると思っている開発者の方もいらっしゃるかもしれません。残念ながら、今回ご説明したように、そうした認識は正しくありません。ツールボックス内のツールを使用するうえで何よりも重要なのは、それぞれのベスト プラクティスに従うことであり、JWT も例外ではないのです。JWT の場合、実績ある高品質なライブラリの選択、ペイロードおよびヘッダーのクレームの検証、適切なアルゴリズムの選択のほか、強力なキーの生成、各 API の細部に対する注意などが必要になります。これだけのベスト プラクティスに対応するのは荷が重すぎると思われる場合は、一部作業を外部プロバイダーに委託するのも一案です。その際にはぜひ Auth0 の利用をご検討ください。外部に委託できない場合は、この記事で挙げた推奨事項について入念に検討しましょう。そして、暗号化の方法は決して自作せず、テストを重ねた実績あるコードを利用するようにしてください。

"JWT は一見シンプルですが、油断してはいけません。JWT を使用するときには、十分に注意を払い、最新のベスト プラクティスに従いましょう"