セキュリティとシリアライズを強化するJava 17の新機能
2021年10月21日
0 分で読めます2020年12月、Java独自のシリアライズ実装が抱える問題について、記事「Javaのシリアライズとデシリアライズ:Javaのデシリアライズ脆弱性を解説」を執筆しました。シリアライズの仕組みはJavaに深く組み込まれているため、一部の実装がどれほど危険なのかを知っておくことが重要です。安全でないデシリアライズでは、クラスパス上のクラスからガジェットチェーンが作られると、任意のコードが実行されるおそれがあります。
新しいLTSバージョンであるJava 17がリリースされました。新機能はこの問題にどのような影響を与えるのでしょうか。また、これらの機能を使ってデシリアライズの脆弱性をより効果的に防げるのでしょうか。
この記事では、Java 17の主な機能を3つ取り上げます。
レコード
Java Flight Recorder(JFR)の改善
JEP 415(Java Enhancement Proposal):コンテキストに応じたデシリアライズフィルター
1. レコード
レコードはJava 14でプレビュー機能として導入され、Java 16で正式リリースされました。ただし、多くの開発者はLTSバージョンへのアップグレードを好むため、Java 17が正式リリースされた今、シリアライズとの関連でレコードについて解説するのは意義があります。
通常のPOJOとは異なり、レコードをデシリアライズするときはコンストラクターを使ってオブジェクトを再作成します。通常のJavaオブジェクトではそうではなく、フレームワークはリフレクションに大きく依存しています。そのため、通常のJavaオブジェクトをデシリアライズする際には、コンストラクターによって実行されるロジックは呼び出されません。詳しくは以前の記事をご覧ください。レコードのデシリアライズでは、オブジェクトの再作成に特別な仕組みは使われません。レコードのコンストラクターに検証ロジックを入れておけば、デシリアライズ時にもそのロジックが適用されることがわかります。
とはいえ、レコードにロジックを入れるべきかどうかは議論の余地があります。誤った実装によって、デシリアライズのガジェットチェーンに関与するガジェットが作られる可能性があります。さらに重要なのは、デシリアライズには引き続きreadObject()関数を使うという点です。そのため、レコードの有無にかかわらず、通常のPOJOではガジェットチェーンの脆弱性が残ります。
2. Javaのデシリアライズフィルター
Javaのデシリアライズ脆弱性に対処するには、シリアライズフィルターを設定できます。この機能は、JEP 290の実装によりJava 9で導入されました。配列のサイズ、グラフの深さ、参照の合計数、ストリームのサイズに上限を設定できます。また、パターンに基づくブロックリストや許可リストを作成し、デシリアライズするクラスを制限することもできます。
このようなフィルターは、JVM全体に適用するか、ストリームごとに設定できます。グローバルフィルターは、JVM引数またはコードで設定できます。以下では、mypackageのすべてのクラスを許可し、それ以外をすべてブロックするフィルターを作成しています。
JVM引数:
コード:
以下のように、特定のストリームにフィルターを設定することもできます。
Java 17以前では、特定のストリームにフィルターを設定すると、そのストリームではグローバルフィルターが上書きされます。グローバルフィルターとストリーム固有のフィルターが組み合わされることはありません。この方法は柔軟性に欠けます。さらに、ライブラリがデシリアライズを行う場合、グローバルフィルターが機能しない可能性があります。
Java 17のコンテキストに応じたデシリアライズフィルター
Java 17では、JEP 415の実装によってデシリアライズフィルターが強化されました。
重要な変更の1つは、SerialFilterFactoryにObjectInputFilter.Configを設定できるようになったことです。このファクトリーはBinaryOperatorである必要があり、特定のストリームにフィルターが追加されたときの処理を定義します。
以下の例では、既存のフィルターと新しいフィルターをデフォルトのマージ方法で統合する、シンプルなファクトリーをConfigに設定しています。この機能を使えば、フィルターをマージするかどうか、またどのようにマージするかを決められます。これにより、先ほど説明した、ライブラリがグローバルフィルターをどのように扱うかという問題を解決できます。
フィルターファクトリーに加えて、Java 17にはフィルターを簡単に作成できる便利なメソッドも用意されています。allowFilter()のrejectFilter()やObjectInputFilterといった関数を使うと、より宣言的で読みやすい方法でフィルターを作成できます。
以下のJava 17のコード例では、これらの新機能を使っています。この例のdeserializeメソッドでは、Gadgetクラスを明示的に拒否しています。GadgetクラスとTwoValueレコードは同じパッケージに属しています。これにより、Gadgetは拒否され、同じパッケージ内のその他のクラスはフィルターによって許可されます。
3. Java Flight Recorderのデシリアライズイベント
Java 17のリリースでは、デシリアライズの悪用に対抗するため、Java Flight Recorder(JFR)も強化されました。このバージョンでは、デシリアライズを監視する専用イベントがサポートされています。ストリーム内のオブジェクトごとにデシリアライズイベントが生成され、実際の型、フィルターの有無、オブジェクトがフィルターにかかったかどうか、オブジェクトの深さ、参照数など、さまざまな情報が記録されます。
これらの情報を使えば、アプリケーション内でデシリアライズが行われているか、プロセスの実行中に実際に何がデシリアライズされているかを確認できます。ただし、このイベントを有効にする必要があります。デフォルトでは記録されないため、次のような設定を行ってください。
先ほどのコード例を使い、今度はGadgetクラスをデシリアライズしてみましょう。Java Flight Recorderとカスタム設定を使ってIntelliJ IDEAでコードを実行すると、次の結果が得られます。

イベントから、デシリアライズ時の実際のオブジェクトの型と、フィルターによってそのオブジェクトが拒否されたことがわかります。JFRのこのデシリアライズイベントの詳しい使い方については、Chris Hegartyによるブログ記事「デシリアライズを監視してアプリケーションのセキュリティを強化する」をご覧ください。
安全でないデシリアライズの悪用に対抗する強力なツールを活用するため、Java 17にアップグレードしましょう
Java 17 LTSリリースには、Javaアプリケーションで悪意のあるデシリアライズを防ぐための重要な改善が含まれています。これらの対策を取り入れるには、Java 17などの新しいバージョンへのアップグレードが欠かせません。それでも、私としてはJava独自のシリアライズは極力避けるべきだと考えています。ただし、使わざるを得ない場合や、Java独自のデシリアライズを実行するライブラリに依存している場合は、今回紹介した方法で自分を守ることができます。
また、既知のデシリアライズガジェットチェーンを含むライブラリや、その他のデシリアライズセキュリティ上の問題があるライブラリをインポートしないようにしてください。
キャプチャー・ザ・フラッグを始めよう
オンデマンドのバーチャル入門ワークショップを視聴して、キャプチャー・ザ・フラッグの課題の解き方を学びましょう。



