シリアライズ
Effective Java 第 3 版の個人的メモ
- 項目 85 Java のシリアライズよりも代替手段を選ぶ
- 項目 86 Serializable を細心の注意を払って実装する
- 項目 87 カスタムシリアライズ形式の使用を検討する
- 項目 88 防御的に readObject メソッドを書く
- 項目 89 インスタンス制御に対しては、readResolve より enum 型を選ぶ
- 項目 90 シリアライズされたインスタンスの代わりに、シリアライズ・プロキシを検討する
1. 項目 85 Java のシリアライズよりも代替手段を選ぶ
1.1. 結論
- シリアライズは使うべきでない
- シリアライズは脆弱でリモートコード実行(RCE)やDoS攻撃などの攻撃の弱点となってしまうから
- 代替手段としてはJSONやProtobufなどのような他の手段を使うべし
- データフォーマットが定まっていてシリアライズよりましだから?
- XML bombもある。Jsonハイジャックもある。
- ユーザコードを叩くかどうかの違い?
- シリアライズが避けられない場合でも、信頼できないデータはデシリアライズするべきでない
- 信頼できないデータは攻撃対象となっている可能性があるから
- シリアライズが避けられないかつ信頼できないデータをデシリアライズしなければならない場合は、フィルタリングをするべし
1.2. シリアライズの脆弱性
- シリアライズは脆弱である
- ガジェット、ガジェットチェーンによって任意のネイティブコードが実行できてしまうことがある
- Javaのシリアライズ可能な型を持つメソッドをガジェット、ガジェットの組み合わせをガジェットチェーンとよぶ
- 実際にガジェットチェーンを用いて任意のネイティブコードが実行する実証実験された
- デシリアライゼーションボムによってDoS攻撃ができてしまうことがある
- デシリアライゼーションボムは、インスタンスのディシリアライズをする場合に、そのフィールドや要素のハッシュコードを計算する必要があることを悪用して、例えば深い入れ子構造になっているHashSetインスタンスなどを作成してデシリアライズさせることで、莫大な計算時間を掛けさせる攻撃手法
1.3. デシリアライズ時のフィルタリング
1.4. 余談
- シリアライズの扱いずらさ
- 送信元と送信先が同じオブジェクトを持たなければならない
- どういう局面でシリアライズを使うでしょうか
- セッション
- システム内通信
- EJB(Enterprise Java Bean)
2. 項目 86 Serializable を細心の注意を払って実装する
2.1. 結論
- Serializableインターフェースは軽く考えて実装するべきではない
- 以下の3つのコストがかかってしまうから
- リリース後のクラスの実装の変更に対する柔軟性の低下
- バグやセキュリティホールの可能性の増大
- テストの負荷の増大
- 継承するために設計されたクラスとインタフェースではSerializableを実装・拡張するべきでない
2.2. Serializable実装に伴う3つのコスト
2.2.1. リリース後のクラスの実装の変更に対する柔軟性の低下
- Serializableを実装したクラスを一旦リリースしてしまうと、その実装を変更することは容易ではない
- クラスをシリアライズ可能にすると、そのシリアライズ形式がクラスの公開APIの一部となり、シリアライズ形式を永久にサポートし続ける必要があるから
- デフォルトのシリアライズ形式の場合は、 privateフィールドも公開されてしまう
- serialVersionUIDを明示的に指定しない場合は、コンパイラはクラス名やクラスが実装しているインタフェース名、publicとprotectedのメンバからserialVersionUIDを生成されるするので、メソッドを一つ追加しただけでserialVersionUIDが変更され、互換性が失われるから。
- javaバージョンによって生成されるserialVersionUIDが違う可能性あり。だから指定しましょう
2.2.2. バグやセキュリティホールの可能性の増大
- バグの可能性が増大する理由は、デシリアライズ時のオブジェクトは通常のコンストラクタを使わずに生成されるため、コンストラクタで保証される不変式が保証されないから。
- セキュリティホールについては、項目85参照
2.2.3. テストの負荷の増大
- 新たなリリースのインスタンスをシリアライズし、古いリリースのデシリアライズ処理でインスタンスが復元できるを確認する必要がある。 また逆に、古いリリースのインスタンスをシリアライズし、新しいリリースのデシリアライズ処理で復元できるかも確認する必要がある
- これらのテストは、新旧のインスタンス間でシリアライズ/デシリアライズできるかというバイナリ互換性に加えて、 動作が意図しているものかどうかというセマンティクス互換性も検査する必要がある
- 互換の一覧
- ソース互換
- バイナリ互換
- 動作の互換性(≒セマンティクス互換性)
2.2.4. 継承するために設計されたクラス・インターフェースに対するSerializable実装
- 継承するために設計されたクラスとインタフェースではSerializableを実装・拡張するべきでない
- ただし上記には例外があり、「例えば、全ての参加者がSerializableを実装しなければならない何らかのフレームワークに参加するためにクラスやインターフェースが主に存在している場合」は破ってもいい
- 例えば、ThrowableはSerializableを実装しているので、リモートメソッド呼び出し(RMI)からの例外を、クライアントに渡せる
- Component、HttpServletもSerializableを実装している
3. 項目 87 カスタムシリアライズ形式の使用を検討する
3.1. 結論
Javaにおけるシリアライズの実装方法は2通りある。