Java Serialization

シリアライゼーションJavaオブジェクト永続化RMIセキュリティ

Java Serialization

Java Serialization

概要

Java Serializationは、Javaオブジェクトの状態をバイトストリームに変換し、後でそのバイトストリームから元のオブジェクトを再構築できるようにする、Java標準の組み込みメカニズムです。オブジェクトの永続化、ネットワーク通信、分散システムでのデータ交換などに使用されます。java.io.Serializableインターフェースを実装することで、オブジェクトをシリアライズ可能にできます。

詳細

Java Serializationは、Java 1.1から導入された機能で、以下の主要コンポーネントで構成されています:

主要なクラスとインターフェース

  • Serializable: マーカーインターフェース。クラスがシリアライズ可能であることを示します
  • ObjectOutputStream: オブジェクトをバイトストリームに書き込むためのクラス
  • ObjectInputStream: バイトストリームからオブジェクトを読み込むためのクラス
  • Externalizable: より細かい制御が必要な場合に使用するインターフェース

重要な概念

  • serialVersionUID: クラスのバージョン管理に使用される一意の識別子
  • transient: シリアライズから除外するフィールドを指定するキーワード
  • readObject/writeObject: カスタムシリアライゼーションロジックを実装するためのメソッド
  • readResolve/writeReplace: シリアライゼーションプロセスを制御するための特殊メソッド

セキュリティの考慮事項

Java Serializationには深刻なセキュリティリスクが存在します:

  1. デシリアライゼーション攻撃: 悪意のあるデータがデシリアライズされると、任意のコードが実行される可能性があります
  2. クラスローディングの脆弱性: データによって決定されるクラスのインスタンス化は攻撃の温床となります
  3. 信頼できないデータの危険性: 外部からのシリアライズデータは常に危険と見なすべきです

メリット・デメリット

メリット

  • Java標準機能: 追加ライブラリ不要で利用可能
  • オブジェクトグラフの完全な保存: 参照関係を含む複雑なオブジェクト構造を保存可能
  • RMI統合: Remote Method Invocationでネイティブサポート
  • 簡単な実装: Serializableインターフェースを実装するだけで基本的な機能が使える
  • バージョン管理: serialVersionUIDによるクラスの互換性管理
  • カスタマイズ可能: readObject/writeObjectで独自のシリアライゼーションロジックを実装可能

デメリット

  • セキュリティリスク: デシリアライゼーション攻撃の脆弱性が深刻
  • パフォーマンス: JSONやProtocol Buffersと比較して遅い
  • Java専用: 他の言語との相互運用性がない
  • ファイルサイズ: バイナリ形式だが、効率的ではない
  • デバッグ困難: バイナリ形式のため、人間が読めない
  • 破壊的変更: クラス構造の変更により互換性が失われやすい
  • 推奨されない: 多くのセキュリティ専門家が使用を避けるよう推奨

参考ページ

書き方の例

Hello World - 基本的なシリアライゼーション

import java.io.*;

// シリアライズ可能なクラス
class Person implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}

public class SerializationExample {
    public static void main(String[] args) {
        Person person = new Person("田中太郎", 30);
        
        // シリアライゼーション
        try (ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("person.ser"))) {
            oos.writeObject(person);
            System.out.println("オブジェクトをシリアライズしました: " + person);
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // デシリアライゼーション
        try (ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("person.ser"))) {
            Person deserializedPerson = (Person) ois.readObject();
            System.out.println("オブジェクトをデシリアライズしました: " + deserializedPerson);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

transientキーワードの使用

import java.io.*;

class User implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String username;
    private transient String password; // シリアライズから除外
    private String email;
    
    public User(String username, String password, String email) {
        this.username = username;
        this.password = password;
        this.email = email;
    }
    
    @Override
    public String toString() {
        return "User{username='" + username + 
               "', password='" + password + 
               "', email='" + email + "'}";
    }
}

public class TransientExample {
    public static void main(String[] args) throws Exception {
        User user = new User("user123", "secretPassword", "[email protected]");
        
        // シリアライゼーション
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(user);
        oos.close();
        
        // デシリアライゼーション
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        User deserializedUser = (User) ois.readObject();
        ois.close();
        
        System.out.println("元のユーザー: " + user);
        System.out.println("復元されたユーザー: " + deserializedUser);
        // passwordフィールドはnullになっている
    }
}

カスタムシリアライゼーション

import java.io.*;
import java.time.LocalDateTime;

class CustomSerializable implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private String data;
    private transient LocalDateTime timestamp;
    
    public CustomSerializable(String data) {
        this.data = data;
        this.timestamp = LocalDateTime.now();
    }
    
    // カスタムシリアライゼーション
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject(); // デフォルトのシリアライゼーション
        oos.writeObject(timestamp.toString()); // LocalDateTimeを文字列として保存
    }
    
    // カスタムデシリアライゼーション
    private void readObject(ObjectInputStream ois) 
            throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); // デフォルトのデシリアライゼーション
        String timestampStr = (String) ois.readObject();
        this.timestamp = LocalDateTime.parse(timestampStr);
    }
    
    @Override
    public String toString() {
        return "CustomSerializable{data='" + data + 
               "', timestamp=" + timestamp + "}";
    }
}

Externalizableインターフェースの実装

import java.io.*;

class ExternalizableExample implements Externalizable {
    private String name;
    private int value;
    private transient String cachedData;
    
    // Externalizableには引数なしコンストラクタが必須
    public ExternalizableExample() {}
    
    public ExternalizableExample(String name, int value) {
        this.name = name;
        this.value = value;
        this.cachedData = "Cached: " + name;
    }
    
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        // 完全に制御されたシリアライゼーション
        out.writeUTF(name);
        out.writeInt(value);
        // cachedDataは保存しない
    }
    
    @Override
    public void readExternal(ObjectInput in) 
            throws IOException, ClassNotFoundException {
        // 完全に制御されたデシリアライゼーション
        name = in.readUTF();
        value = in.readInt();
        // cachedDataを再構築
        cachedData = "Cached: " + name;
    }
    
    @Override
    public String toString() {
        return "ExternalizableExample{name='" + name + 
               "', value=" + value + 
               ", cachedData='" + cachedData + "'}";
    }
}

シリアライゼーションフィルターによるセキュリティ強化

import java.io.*;

public class SecureDeserialization {
    public static void main(String[] args) throws Exception {
        // 許可リストベースのフィルター設定
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
            "java.lang.*;java.util.*;!*" // java.langとjava.utilのみ許可
        );
        
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject("安全な文字列");
        oos.writeObject(42);
        oos.close();
        
        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bais);
        
        // フィルターを適用
        ois.setObjectInputFilter(filter);
        
        try {
            String str = (String) ois.readObject();
            Integer num = (Integer) ois.readObject();
            System.out.println("デシリアライズ成功: " + str + ", " + num);
        } catch (InvalidClassException e) {
            System.err.println("フィルターによってブロックされました: " + e.getMessage());
        }
        ois.close();
    }
}

シリアライゼーションプロキシパターン

import java.io.*;

// 不変クラスの安全なシリアライゼーション
public final class Period implements Serializable {
    private static final long serialVersionUID = 1L;
    
    private final Date start;
    private final Date end;
    
    public Period(Date start, Date end) {
        this.start = new Date(start.getTime());
        this.end = new Date(end.getTime());
        if (this.start.compareTo(this.end) > 0) {
            throw new IllegalArgumentException("開始日は終了日より前でなければなりません");
        }
    }
    
    // シリアライゼーションプロキシ
    private static class SerializationProxy implements Serializable {
        private static final long serialVersionUID = 1L;
        
        private final Date start;
        private final Date end;
        
        SerializationProxy(Period p) {
            this.start = p.start;
            this.end = p.end;
        }
        
        // プロキシからPeriodインスタンスを再構築
        private Object readResolve() {
            return new Period(start, end); // コンストラクタで検証
        }
    }
    
    // Periodインスタンスの代わりにプロキシを書き込む
    private Object writeReplace() {
        return new SerializationProxy(this);
    }
    
    // 直接的なデシリアライゼーションを防ぐ
    private void readObject(ObjectInputStream stream) 
            throws InvalidObjectException {
        throw new InvalidObjectException("プロキシが必要です");
    }
}