1. 程式人生 > >Effective Java 第三版——88. 防禦性地編寫READOBJECT方法

Effective Java 第三版——88. 防禦性地編寫READOBJECT方法

lock pri specified 什麽 事情 his 生成 tails print

Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
註意,書中的有些代碼裏方法是基於Java 9 API中的,所以JDK 最好下載 JDK 9以上的版本。

技術分享圖片

88. 防禦性地編寫READOBJECT方法

條目 50 裏有一個不可變的日期範圍類,它包含一個可變的私有Date屬性。 該類通過在其構造方法和訪問器中防禦性地拷貝Date對象,竭盡全力維持其不變性(invariants and immutability)。 代碼如下所示:

// 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 Serializable 添加到類聲明中就可以了。但是,如果這樣做,該類不再保證它的關鍵不變性了。

問題是readObject方法實際上是另一個公共構造方法,它需要與任何其他構造方法一樣的小心警惕。 正如構造方法必須檢查其參數的有效性(條目 49)並在適當的地方對參數防禦性拷貝(條目 50),readObject方法也要這樣做。 如果readObject方法無法執行這兩個操作中的任何一個,則攻擊者違反類的不變性是相對簡單的事情。

簡而言之,readObject是一個構造方法,它將字節流作為唯一參數。 在正常使用中,字節流是通過序列化正常構造的實例生成的。當readObject展現一個字節流時,問題就出現了,這個字節流是人為構造的,用來生成一個違反類不變性的對象。 這樣的字節流可用於創建一個不可能的對象,該對象無法使用普通構造方法創建。

假設我們只是將implements Serializablet添加到Period類聲明中。 然後,這個醜陋的程序生成一個Period實例,其結束時間在其開始時間之前。 對byte類型的值進行強制轉換,其高階位被設置,這是由於Java缺乏byte字面量,並且錯誤地決定對byte類型進行簽名:

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);
    }
  }
}

用於初始化serializedForm的字節數組字面量(literal)是通過序列化正常的Period實例,並手動編輯生成的字節流生成的。 流的細節對於該示例並不重要,但是如果好奇,則在《Java Object Serialization Specification》[序列化,6]中描述了序列化字節流格式。 如果運行此程序,它會打印Fri Jan 01 12:00:00 PST 1999 - Sun Jan 01 12:00:00 PST 1984。只需聲明Period類為可序列化,我們就可以創建一個違反其類不變性的對象。

要解決此問題,請為Period提供一個readObject方法,該方法調用defaultReadObject,然後檢查反序列化對象的有效性。如果有效性檢查失敗,readObject方法拋出InvalidObjectException異常,阻止反序列化完成:

// 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實例,然後將額外引用附加到Period實例內部的私有Date屬性。 攻擊者從ObjectInputStream中讀取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方法來執行防禦性拷貝。 需要這兩個細節來保護Period免受攻擊(條目 50)。 另請註意,final屬性無法進行防禦性拷貝。 要使用readObject方法,我們必須使start和end屬性不能是final類型的。 這是不幸的,但它是這兩個中較好的一個做法。 使用新的readObject方法並從startend屬性中刪除final修飾符後,MutablePeriod類不再無效。 上面的攻擊程序現在生成如下輸出:

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

下面是一個簡單的石蕊測試(litmus test),用於確定類的默認readObject方法是否可接受:你是否願意添加一個公共構造方法,該構造方法把對象中每個非瞬時狀態的屬性值作為參數,並在沒有任何驗證的情況下,將值保存在屬性中?如果沒有,則必須提供readObject方法,並且它必須執行構造方法所需的所有有效性檢查和防禦性拷貝。或者,可以使用序列化代理模式(serialization proxy pattern))(條目 90)。強烈推薦使用這種模式,因為它在安全反序列化方面花費了大量精力。

readObject方法和構造方法還有一個相似之處,它們適用於非final可序列化類。 與構造方法一樣,readObject方法不能直接或間接調用可重寫的方法(條目 19)。 如果違反此規則並且重寫了相關方法,則重寫方法會在子類狀態被反序列化之前運行。 程序可能會導致失敗[Bloch05,Puzzle 91]。

總而言之,無論何時編寫readObject方法,都要采用這樣一種思維方式,即正在編寫一個公共構造方法,該構造方法必須生成一個有效的實例,而不管給定的是什麽字節流。不要假設字節流一定表示實際的序列化實例。雖然本條目中的示例涉及使用默認序列化形式的類,但是所引發的所有問題都同樣適用於具有自定義序列化形式的類。下面是編寫readObject方法的指導原則:

  • 對於具有必須保持私有的對象引用屬性的類,防禦性地拷貝該屬性中的每個對象。不可變類的可變組件屬於這一類別。

  • 檢查任何不變性,如果檢查失敗,則拋出InvalidObjectException異常。 檢查應再任何防禦性拷貝之後。

  • 如果必須在反序列化後驗證整個對象圖(object graph),那麽使用ObjectInputValidation接口(在本書中沒有討論)。

  • 不要直接或間接調用類中任何可重寫的方法。

Effective Java 第三版——88. 防禦性地編寫READOBJECT方法