第88項:保護的にreadObject方法を編纂する。


第50項は可変的なプライベートDateドメインを含む可変日付範囲クラスを紹介しています。このクラスは、そのコンストラクタとアクセス方法(accessor)において、Dateオブジェクトを保護的にコピーすることにより、その制約条件と可変性を極力維持する。以下はこのクラスです。
// Immutable class that uses defensive copying
public final class Period {
    private final Date start;
    private final Date end;
    /**
    * @param start the beginning of the period
    * @param end the end of the period; must not precede start
    * @throws IllegalArgumentException if start is after end
    * @throws NullPointerException if start or end is 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(start + " after " + end);
    }
    public Date start () { return new Date(start.getTime()); }
    public Date end () { return new Date(end.getTime()); }
    public String toString() { return start + " - " + end; }
    ... // Remainder omitted
}
  はこのクラスを序列化できるようにすると決めたとします。Periodオブジェクトの物理的表現法は、ちょうどその論理データの内容を反映しているので、デフォルトの順序化形式を使用するのは、あまり合理的ではない(第87項)。したがって、このクラスを逐次的にするためには、クラスの声明に「implements Serialzable」という文字を追加する必要があるようです。しかし、もしあなたが本当にそうすれば、このクラスはもう鍵となる制約を保証しなくなります。
問題は、readObject方法は実際には別の公有のコンストラクタに相当し、他のコンストラクタのように、同様の注意事項をすべて要求することである。コンストラクタは、そのパラメータの有効性を確認しなければならない(第49項)、必要に応じてパラメータを保護コピーする(第50項)と同様に、readObject方法も必要である。readObject法がこの2つを達成できないなら、攻撃者にとっては、このような制約条件に違反するのは比較的簡単です。
厳密には言いませんが、readObjectはバイトフローを唯一のパラメータとしています。通常の使用では、バイトストリームは、シーケンス化された通常の構造の例によって生成される。readObjectが人工的に生成されたクラスの制約に反するバイトストリームに直面すると、問題が発生し、クラスの制約条件に反するオブジェクトが生成される。このようなバイトストリームを使用して、不可能なオブジェクトを作成することができます。オブジェクトは通常のアーキテクチャ関数を使用して作成できません。
私たちは簡単に「implemens Serializable」をPeriodの種類の声明に追加するだけだと仮定します。そして、この醜いプログラムは、開始時間よりも早く終了するPeriodの例を生成する。上位に設定されたバイト値の強制変換は、Javaのバイト文字不足と不幸な意思決定との結合によるバイトタイプ署名の結果である。
public class BogusPeriod {
    // Byte stream couldn't have come from a real Period instance!
    private static final byte[] serializedForm = {
        (byte)0xac, (byte)0xed, 0x00, 0x05, 0x73, 0x72, 0x00, 0x06,
        0x50, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x40, 0x7e, (byte)0xf8,
        0x2b, 0x4f, 0x46, (byte)0xc0, (byte)0xf4, 0x02, 0x00, 0x02,
        0x4c, 0x00, 0x03, 0x65, 0x6e, 0x64, 0x74, 0x00, 0x10, 0x4c,
        0x6a, 0x61, 0x76, 0x61, 0x2f, 0x75, 0x74, 0x69, 0x6c, 0x2f,
        0x44, 0x61, 0x74, 0x65, 0x3b, 0x4c, 0x00, 0x05, 0x73, 0x74,
        0x61, 0x72, 0x74, 0x71, 0x00, 0x7e, 0x00, 0x01, 0x78, 0x70,
        0x73, 0x72, 0x00, 0x0e, 0x6a, 0x61, 0x76, 0x61, 0x2e, 0x75,
        0x74, 0x69, 0x6c, 0x2e, 0x44, 0x61, 0x74, 0x65, 0x68, 0x6a,
        (byte)0x81, 0x01, 0x4b, 0x59, 0x74, 0x19, 0x03, 0x00, 0x00,
        0x78, 0x70, 0x77, 0x08, 0x00, 0x00, 0x00, 0x66, (byte)0xdf,
        0x6e, 0x1e, 0x00, 0x78, 0x73, 0x71, 0x00, 0x7e, 0x00, 0x03,
        0x77, 0x08, 0x00, 0x00, 0x00, (byte)0xd5, 0x17, 0x69, 0x22,
        0x00, 0x78
    };
    public static void main(String[] args) {
        Period p = (Period) deserialize(serializedForm);
        System.out.println(p);
    }
    // Returns the object with the specified serialized form
    static Object deserialize(byte[] sf) {
        try {
            return new ObjectInputStream(new ByteArrayInputStream(sf)).readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new IllegalArgumentException(e);
        }
    }
}
SerializableFormを初期化するために用いられたbyte配列定数は、まず正常なPeriodのインスタンスに対して順序付けを行い、得られたバイトストリームを手動で編集することによって生成される。この例では、バイトストリームの詳細は、A片を重要としないが、気になるなら、Java Object Serialzation Specizationでは、プログレッシブバイトストリームフォーマットに関する記述情報を調べることができる。このプログラムを実行すると「Fri Jan 01 12:00 PST 1999-Sun Jan 01 12:00 PST 1984」が印刷されます。Periodを逐次的に宣言すると、その制約に違反するオブジェクトを作成します。
この問題を修正するために、PeriodにreadObject方法を提供してもいいです。この方法はまずdefault ReadObjectを呼び出してから、逆順序化されたオブジェクトの有効性を確認します。有効性検査が失敗したら、readObject方法はInvalidObject Exceptioの異常を投げ出して、プログレッシブを完成することを防止します。
// readObject method with validity checking - insufficient!
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Check that our invariants are satisfied
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}
このような修正は攻撃者が無効なPeriodを作成することを避けましたが、ここにはもっと微妙な問題が隠れています。バイトストリームを偽造することによって、可変Periodのインスタンスを作成することは依然として可能であり、やり方は、バイトストリームが効果的にPeriodのインスタンスで開始され、次いで2つの追加的な参照を追加して、Periodのインスタンス内の2つのプライベートのDateドメインを指す。攻撃者は、Object InputStreamからPeriodの例を読み出し、その後に付加された「悪意のある作成の対象参照」を読み出す。これらのオブジェクト参照は、攻撃者がPeriodオブジェクト内部のプライベートDateドメインに参照されるオブジェクトにアクセスできるようにする。これらのDateの例を変更することにより、攻撃者はPeriodの例を変更することができる。次のクラスはこの攻撃をデモしました。
public class MutablePeriod {
    // A period instance
    public final Period period;
    // period's start field, to which we shouldn't have access
    public final Date start;
    // period's end field, to which we shouldn't have access
    public final Date end;
    public MutablePeriod() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream out = new ObjectOutputStream(bos);
            // Serialize a valid Period instance
            out.writeObject(new Period(new Date(), new Date()));
            /*
            * Append rogue "previous object refs" for internal
            * Date fields in Period. For details, see "Java
            * Object Serialization Specification," Section 6.4.
            */
            byte[] ref = { 0x71, 0, 0x7e, 0, 5 }; // Ref #5
            bos.write(ref); // The start field
            ref[4] = 4; // Ref # 4
            bos.write(ref); // The end field
            // Deserialize Period and "stolen" Date references
            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);
        }
    }
}
  攻撃の結果を確認するには、次の手順を実行してください。
public static void main(String[] args) {
    MutablePeriod mp = new MutablePeriod();
    Period p = mp.period;
    Date pEnd = mp.end;
    // Let's turn back the clock
    pEnd.setYear(78);
    System.out.println(p);
    // Bring back the 60s!
    pEnd.setYear(69);
    System.out.println(p);
}
  は私のタイムゾーンにあります。このプログラムを実行すると以下の出力が発生します。
Wed Nov 22 00:21:29 PST 2017-Wed Nov 22 00:21:29 PST 1978 Wed Nov 22 00:21:29 PST 2017-Sat Nov 22 00:21:29 PST 1969
Periodのインスタンスが作成された後、その制約条件は破壊されていませんが、任意にその内部コンポーネントを修正することが可能です。攻撃者が可変のPeriodのインスタンスを獲得すると、彼はこのインスタンスを「安全性はPeriodの不変性に依存する」クラスに渡すことができ、より大きな被害をもたらします。この推論は無理ではない:実際には多くの種類の安全性がStringの不変性に依存している。
問題の根源は、PeriodのreadObject方法が十分な保護コピーを完成していないことにある。オブジェクトが逆順序化される場合、クライアントが所有してはいけないオブジェクト参照の任意のフィールドを保護コピーすることが重要である。したがって、個々の順序付け可能な可変クラスに対して、プライベートの可変構成が含まれている場合、そのreadObject方法では、これらのコンポーネントを保護コピーしなければならない。次のreadObject法はPeriodの制約条件が破壊されないことを保証して、その不変性を維持します。
// readObject method with defensive copying and validity checking
private void readObject(ObjectInputStream s) throws IOException, ClassNotFoundException {
    s.defaultReadObject();
    // Defensively copy our mutable components
    start = new Date(start.getTime());
    end = new Date(end.getTime());
    // Check that our invariants are satisfied
    if (start.compareTo(end) > 0)
        throw new InvalidObjectException(start +" after "+ end);
}
保護コピーは有効性検査の前に行われます。そして、Dateのclone方法を使って保護コピーを行っていません。この2つの詳細は攻撃からPeriodを守るために必要です。同時に、finalドメインに対して、保護コピーは不可能であることにも注目したい。readObject方法を使うためには、startとendドメインを非finalにしなければなりません。残念ですが、これは比較的いいやり方です。この新しいreadObjectの方法があり、startとendドメインのfinal修繕符を外した後、Mutable Period類は有効ではなくなります。この時、上の攻撃プログラムはこのような出力が発生します。
Wed Nov 22 00:23:41 PST 2017-Wed Nov 22 00:23:41 PST 2017 Wed Nov 22 00:23:41 PST 2017-Wed Nov 22 00:23:41 PST 2017
これは簡単なリトマステストで、デフォルトのreadObject方法があるクラスに適用されているかどうかを判断するために使用されます。オブジェクト内の非瞬時フィールド毎の値をパラメータとして追加し、フィールドに値を格納して、検証を行わないようにしますか?ない場合は、readObject方法を提供しなければならず、構造関数に必要なすべての有効性チェックと保護コピーを実行しなければならない。あるいは、プログレッシブプロキシモード(serialization proxy pattern)を使用してもいいです。このモードを使用することを強く推奨します。安全反秩序化に多くのエネルギーが必要です。
  readObject法とコンストラクタの間にもう一つの類似点があります。それらは非finalの序次的なクラスに適用されます。構造関数と同様に、readObject方法は、カバー可能な方法を直接または間接的に呼び出すことができない(第19項)。この規則に違反し、関連方法を書き換えた場合、書き換え方法は、サブクラスの状態が逆順序化される前に実行されます。プログラムが失敗する恐れがあります。
要するに、あなたがreadObject方法を編纂するたびに、こう考えなければなりません。公有のコンストラクタを編纂しています。それにどんなバイトフローを伝達するにも、有効なインスタンスを生成しなければなりません。このバイトストリームは、必ずしも真にプロビジョニングされた例を表していると仮定してはならない。本項の例では、クラスはデフォルトのプログレッシブ形式を使用していますが、議論されている可能性のあるすべての問題は、カスタムのプログレッシブ形式を使用するクラスにも適用されます。以下は要約の形式でいくつかの指導方針を示し、よりロバストなreadObject方法を編纂するのに役立つ。
  • は、オブジェクト参照領域に対してプライベートクラスとして維持しなければならず、これらのドメインの各オブジェクトを保護的にコピーする。可変ではない種類の可変コンポーネントはこのカテゴリに属します。
  • は、任意の制約条件に対して、検査に失敗した場合、InvalidObject Exception異常を投げます。これらの検査動作はすべての保護性コピーの後に続くべきです。
  • 対象図全体が逆順序化された後に検証されなければならない場合は、Object Input Validationインターフェースを使用するべきである(本では議論しない)。
  • は、直接的な方法であれ、間接的な方法であれ、クラス内でカバー可能な方法を呼び出さない。