【Effective Java】項目76:防御的に readObject を書く

ストリームからオブジェクトをデシリアライズする readObject メソッドは実質的に public コンストラクタとして機能します。 そのため、クラスのコンストラクタで検査している正当性や不変式を readObject にも実装する必要があります。

Period クラスの例

項目39では、開始日時と終了日時の組み合わせを示すための Period クラスを実装しました。 Period インスタンスは次の不変性があり、どんな Period インスタンスも次の条件を満たすことが保証されています。

start プロパティは end プロパティ以前の日時を示している

public class Period {
    private final Date start;
    private final Date end;

    /**
     * @param start 期間の開始
     * @param end   期間の終わり。開始より前であってはならない。
     * @throws IllegalArgumentException start が end の後の場合。
     * @throws NullPointerException     start か end が null の場合
     */
    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(this.start + " after " + this.end);
        }
    }

    public Date getStart() {
        return new Date(start.getTime());
    }

    public Date getEnd() {
        return new Date(end.getTime());
    }
}

Period クラスをシリアライズするためには単に Serializable インタフェースを実装するだけです。 しかし、その方法では Period クラスの不変性を破る不正な Period インスタンスを生成することが可能になってしまいます。

項目39 ではコンストラクタとアクセッサにおいて防御的コピーを実装し、クラスの不変式が破られないように努力しました。 この努力が readObject メソッドにも必要になります。

不変式のチェック

単に Serializable を implements しただけではバイトストリームを改変することによって、簡単に Period クラスの不変性を突破することができます。 シリアライズによってバイトストリーム化された Date のバイナリに直接手を加え、Date インスタンスの値を改変します。

この攻撃を防ぐためには readObject メソッドで不変式をチェックし、InvalidObjectException をスローするようにします。

// 不変式をチェックする readObject メソッド
private void readObject(ObjectInputStream stream)
        throws IOException, ClassNotFoundExcpetion {
    stream.defaultReadObject();

    if (start.compareTo(end) > 0) {
        throw new InvalidObjectException(start + " after " + end);
    }
}

防御的コピー

不変式のチェックによって不正な Period インスタンスの生成を防ぐことができます しかし、これでは防御が不完全です。

正当な Period インスタンスを含むバイトストリームのあとに、Period インスタンスの private な Date フィールドへの参照を追加したバイトストリームを作ると可変な Period インスタンスを生成できてしまいます。

具体的には次のようなコードになります。

public class MutablePeriod {
    public final Period period;

    public final Date start;
    public final Date end;

    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // Period インスタンスの書き込み
            out.writeObject(new Period(new Date(), new Date()));
            // Period インスタンスの特定のプロパティへの参照を作成
            byte[] ref = {0x71, 0, 0x7e, 0, 5};
            bos.write(ref);
            ref[4] = 4;
            bos.write(ref);

            // 可変な Period インスタンスの生成
            ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
            period = (Period) in.readObject();
            start = (Date) in.readObject();
            end = (Date) in.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new AssertionError(e);
        }
    }
}

この MutablePeriod に対して、次のようなコードを実行すると簡単に Period の start と end を書き換えることができます。

MutablePeriod period = new MutablePeriod();
period.start.setTime(1);
period.end.setTime(0);

これを防ぐため項目39で用いた防御的コピーを利用します。

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        stream.defaultReadObject();

        start = new Date(this.start.getTime());
        end = new Date(this.end.getTime());

        if (this.start.compareTo(this.end) > 0) {
            throw new InvalidObjectException(this.start + " after " + this.end);
        }
    }

正当性検査を行う前に防御的コピーが行われることに注意してください。

また、防御的コピーを行うと、フィールドを final にすることはできません。 これは望ましいものではないかもしれませんが、攻撃を許す状態にしておくよりはだいぶましです。

writeUnshared, readUnshared

このようなオブジェクト参照による攻撃への対策として Java 4 で writeUnshared と readUnshared が追加されています。 しかし、これらは項目77で解説する、高度な攻撃に対して脆弱性を持っているので利用しないでください。

基準

防御的に readObject を書くかどうかは、同様のコンストラクタを不変式のチェックなしで公開するかどうか?という視点で考えてください。 もし、そのようなコンストラクタを書くことに不安があるならば、防御的に readObject を書く必要があります。

readObject を書くことはパブリックなコンストラクタを追加していると考えてください。 引数のバイトストリームは正当な物とは限りませんし、実際にシリアライズされたインスタンスである保証はありません。