85.Javaでシリアライズするより、他の方法を考えよ
シリアライズは脆弱
シリアライゼーションの根本的な問題は、攻撃可能な個所が大きすぎ、その個所もコンスタントに広がっているという点にある。
オブジェクトグラフ(参照による結びつきをもったオブジェクト群というイメージ)は、ObjectInputStreamのreadObjectメソッドで、復元される。
このメソッドでは、型がSerializableを実装している限り、クラスパス上のほぼすべての型のオブジェクトをインスタンス化することができる。
バイトストリームをデシリアライズする過程で、このメソッドはクラスパス上にありSerializeを実装しているあらゆる型のコードを実行しできる。よって、これらすべての型が攻撃対象となる。
ガジェット、ガジェットチェーン
攻撃対象となりうるserializableなクラスから脆弱性を取り除いたとしても、アプリケーション自体はまだ脆弱であるかもしれない。
serializableな型のメソッドから、危険な処理をしうるメソッド(ガジェットと呼ばれる)を組み合わせて、ガジェットチェーンなるものを作れば、時に、任意のネイティブコードが実行できるようなことも起こる。実際に、2016年にSan Francisco Metropolitan Transit Agency Municipal Railwayに対してこの仕組みを使ったハックがなされた。
デシリアライゼーションボム
上記のようにガジェットを用いずとも、デシリアライズに多大な時間がかかるストリームは簡単に作れて、これでDoS攻撃ができる。
そのようなストリームはデシリアライゼーションボムといわれる。
// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // Method omitted for brevity
}
上記のコードでは、1つのHashSetが2つのHashSetの要素を持っているという構造が100階層続いている。
HashSetインスタンスを生成するときには、各要素のhashcodeを演算せねばならない。よって、2の100乗回のhashCodeの呼び出しが必要となり、全然処理が終わらなくなる。
対処法は?
シリアライズに関する、上記のような攻撃への対処はどのようにすればよいか?
他の仕組みを使う
一番の方法はデシリアライズをしないことだ。
新たに書くシステムでJavaのシリアライズを使う理由は一つもない。
バイトシーケンスとオブジェクトを翻訳するベターな仕組みが存在しており、それらはcross-platform structured-data representationsと呼ばれる。
JSONとProtobuf
代表的なcross-platform structured-data representationsはJSONとProtobufというものである。
JSONとProtobufで最も重要な相違点は、JSONはテキストベースで人間が読める形である一方、Protobufはバイナリでより効率的である点だ。
信頼できないデータはデシリアライズしない
レガシーなシステムの保守開発などでは、Javaでのシリアライズを避けられないかもしれない。
そういったときには、信頼できないデータはデシリアライズしない、という対策を取るべきだ。
特に、信頼できないソースからのRMIトラフィックは受け付けてはならない。
デシリアライズ時のフィルタリング機能を使う
Javaでのシリアライズを避けられず、デシリアライズするデータの安全性も怪しい場合は、オブジェクトのデシリアライゼーションフィルタであるjava.io.ObjectInputFilter(Java9から導入され、6,7,8にもバックポートされた)を使用すべきだ。
これはストリームがデシリアライズされる前にフィルタリングしてくれる機能を提供してくれる。
クラス単位でリジェクトするかアクセプトするか決められる。この時、ホワイトリスト形式かブラックリスト形式か選択することができるが、ブラックリストは既知の脅威からしか防御できないのでホワイトリストを採用すべき。
フィルタリング機能は、過度なメモリの使用も防いでくれ、過度に深いオブジェクトグラフも防いでくれるが、前述のデシリアライゼーションボムを防ぐ機能はない。