Java オブジェクト シリアライゼーションの理解を深めることで得られる 5 つの知見
Java オブジェクト シリアライゼーションは、JDK 1.1 から組み込まれている仕組みです。この仕組みは、Java オブジェクトの状態をバイト配列に変換し、格納したり通信したりするのに役立ちます。この配列を復元することで、当初の状態に戻すことができます。
シリアライゼーションの仕組みは、ObjectInputStream クラスと ObjectOutputStream クラス、そして Serializable インターフェースを用いて実現されています。この仕組みは非常に便利ですが、セキュリティ面でのリスクも孕んでいます。
1. シリアライゼーションはクラスリファクタリングに対応する
Java シリアライゼーションは、一定範囲内のクラスの変更やリファクタリングを許可します。具体的には、以下のような変更が可能です:
- 新フィールドの追加
- static フィールドへの変更
- transient フィールドへの変更
ただし、フィールドを削除したり、型を変更したりする場合、serialVersionUID を明示的に指定する必要があります。
2. シリアライゼーションは不安全である
Java シリアライゼーションされたデータは、完全にドキュメント化されており、容易に復元可能です。特に RMI を通じて送信されるオブジェクトのプライベートフィールドは、通常は平文で送信されます。
このリスクを軽減するため、writeObject と readObject メソッドをオーバーライドし、データを暗号化することができます。
package com.example;
public class User implements java.io.Serializable {
private String name;
private String familyName;
private int years;
private User partner;
public User(String name, String familyName, int years) {
this.name = name;
this.familyName = familyName;
this.years = years;
}
public String getName() { return name; }
public String getFamilyName() { return familyName; }
public int getYears() { return years; }
public User getPartner() { return partner; }
public void setName(String value) { name = value; }
public void setFamilyName(String value) { familyName = value; }
public void setYears(int value) { years = value; }
public void setPartner(User value) { partner = value; }
private void writeObject(java.io.ObjectOutputStream stream)
throws java.io.IOException {
years = years << 2;
stream.defaultWriteObject();
}
private void readObject(java.io.ObjectInputStream stream)
throws java.io.IOException, ClassNotFoundException {
stream.defaultReadObject();
years = years << 2;
}
}
3. シリアライゼーションされたデータを署名と封印できる
敏感データを保護するためには、単なる暗号化ではなく、データを署名したり封印したりする必要があります。
Java では、java.security.SignedObject と javax.crypto.SealedObject を用いて、この目的を達成することができます。
import java.security.SignedObject;
import javax.crypto.SealedObject;
public class SecureUser implements java.io.Serializable {
private String name;
private String familyName;
private User partner;
public SecureUser(String name, String familyName) {
this.name = name;
this.familyName = familyName;
}
public String getName() { return name; }
public String getFamilyName() { return familyName; }
public User getPartner() { return partner; }
public void setName(String value) { name = value; }
public void setFamilyName(String value) { familyName = value; }
public void setPartner(User value) { partner = value; }
private Object writeReplace()
throws java.io.ObjectStreamException {
return new SealedObject(this, "AES", new byte[16]);
}
}
4. シリアライゼーションにプロキシを組み込める
プロキシを用いて、シリアライゼーションされたデータを制御することができます。これにより、データの転送や格納を効率的に行うことができます。
class UserProxy implements java.io.Serializable {
private String userData;
public UserProxy(User original) {
userData = original.getName() + "," + original.getFamilyName();
if (original.getPartner() != null) {
User partner = original.getPartner();
userData += "," + partner.getName() + "," + partner.getFamilyName();
}
}
private Object readResolve()
throws java.io.ObjectStreamException {
String[] parts = userData.split(",");
User user = new User(parts[0], parts[1]);
if (parts.length > 3) {
User partner = new User(parts[2], parts[3]);
user.setPartner(partner);
partner.setPartner(user);
}
return user;
}
}
public class User implements java.io.Serializable {
public User(String name, String familyName) {
this.name = name;
this.familyName = familyName;
}
private Object writeReplace()
throws java.io.ObjectStreamException {
return new UserProxy(this);
}
private String name;
private String familyName;
private User partner;
}
5. トラストするが検証もする
シリアライゼーションされたデータを鵜呑みにせず、検証を行う必要があります。
Java では、ObjectInputValidation インターフェースを用いて、反復復元後のデータを検証できます。
public class ValidatedUser implements java.io.Serializable, java.io.ObjectInputValidation {
private String name;
private String familyName;
private int years;
public ValidatedUser(String name, String familyName, int years) {
this.name = name;
this.familyName = familyName;
this.years = years;
}
public void validateObject()
throws java.io.InvalidObjectException {
if (years < 0 || years > 150) {
throw new java.io.InvalidObjectException("年齢の範囲が不正です。");
}
}
}