ITEM 88: 防御實(shí)現(xiàn) readObject 方法

ITEM 88: WRITE READOBJECT METHODS DEFENSIVELY
??item 50 包含一個具有可變私有日期字段的不可變?nèi)掌诜秶?。這個類通過在它的構(gòu)造函數(shù)和訪問器中防御性地復(fù)制 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 
}

??假設(shè)您希望這個類是可序列化的。因?yàn)?Period 對象的物理表示準(zhǔn)確地反映了其邏輯數(shù)據(jù)內(nèi)容,所以使用默認(rèn)的序列化形式(item 87)也不是不合理的。因此,要使類可序列化,似乎只需添加 implements serializable。但是,如果您這樣做,該類將不再保證它的關(guān)鍵不變量。問題是,readObject 方法實(shí)際上是另一個公共構(gòu)造函數(shù),它需要像其他構(gòu)造函數(shù)一樣小心。正如構(gòu)造函數(shù)必須檢查其參數(shù)的有效性(item 49),并在適當(dāng)?shù)牡胤綄?shù)進(jìn)行防御性復(fù)制(item 50),readObject 方法也必須如此。如果readObject 方法沒有做到這兩件事中的任何一件,那么攻擊者違反類的不變量是相對簡單的事情。
??簡單地說,readObject 是一個構(gòu)造函數(shù),它只接受字節(jié)流作為參數(shù)。在正常使用中,字節(jié)流是通過序列化一個正常構(gòu)造的實(shí)例來生成的。當(dāng)向 readObject 提供一個字節(jié)流時,問題就出現(xiàn)了,該字節(jié)流是人為構(gòu)造來生成違反其類的不變量的對象的。這樣的字節(jié)流可以用來創(chuàng)建一個不可能的對象,這是使用普通構(gòu)造函數(shù)無法創(chuàng)建的。
??假設(shè)我們只是將 implements Serializable 添加到 Period 的類聲明中。然后,這個丑陋的程序?qū)⑸梢粋€周期實(shí)例,它的結(jié)束先于它的開始。對字節(jié)值的強(qiáng)制轉(zhuǎn)換,其高階位被設(shè)置,是 Java 缺乏字節(jié)文字的結(jié)果,加上不幸的決定,使字節(jié)類型簽名:

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 的字節(jié)數(shù)組文字是通過序列化一個普通的 Period 實(shí)例并手動編輯產(chǎn)生的字節(jié)流生成的。流的細(xì)節(jié)對于這個示例并不重要,但是如果您好奇的話,序列化字節(jié)流格式在 Java 對象序列化規(guī)范[serialization, 6]中有描述。如果你運(yùn)行這個程序,它打印 1999 年 1 月 1 日星期五12:00:00 PST - 1984年1月1日星期日12:00:00 PST。簡單地聲明 Period serializable 使我們能夠創(chuàng)建違反其類不變量的對象。
??要解決這個問題,為Period提供一個readObject方法,該方法調(diào) 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); 
}

??雖然這樣可以防止攻擊者創(chuàng)建無效的 Period 實(shí)例,但仍然存在一個更微妙的問題??梢酝ㄟ^創(chuàng)建一個字節(jié)流來創(chuàng)建可變的 Period 實(shí)例,該字節(jié)流以有效的 Period 實(shí)例開始,然后附加額外的引用到 Period 實(shí)例內(nèi)部的私有日期字段。攻擊者從ObjectInputStream 中讀取 Period 實(shí)例,然后讀取附加到流中的“流氓對象引用”。這些引用使攻擊者能夠訪問由 Periodobject 中的私有日期字段引用的對象。通過改變這些 Date 實(shí)例,攻擊者可以改變 Period 實(shí)例。下面的類演示了這種攻擊:

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

??在我的語言環(huán)境中,運(yùn)行這個程序會產(chǎn)生以下輸出:

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

??雖然創(chuàng)建周期實(shí)例時其不變量保持不變,但可以隨意修改其內(nèi)部組件。一旦擁有了一個可變的 Period 實(shí)例,攻擊者可能會將該實(shí)例傳遞給一個依賴于 Period 的不變性來保證其安全性的類,從而造成極大的傷害。這并不是牽強(qiáng)附會的:有些類依賴于字符串的不變性來保證其安全性。
??問題的根源是 Period 的 readObject 方法沒有做足夠的防御性復(fù)制。反序列化對象時,關(guān)鍵是要防御性地復(fù)制任何包含客戶端不能擁有的對象引用的字段。因此,每個包含私有可變組件的可序列化不可變類必須防御性地在其 readObject 方法中復(fù)制這些組件。下面的 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);
}

  注意,防御復(fù)制是在有效性檢查之前執(zhí)行的,并且我們沒有使用 Date 的克隆方法來執(zhí)行防御復(fù)制。這兩個細(xì)節(jié)都是保護(hù)周期免受攻擊所必需的(item 50)。還要注意,對final 字段不可能進(jìn)行防御性復(fù)制。要使用 readObject 方法,我們必須使 start 和 end字段是非 final 的。這是不幸的,但這只是兩害相權(quán)取其輕。有了新的 readObject 方法并從 start 和 end 字段中刪除了最后一個修飾符后,MutablePeriod 類就變得無效了。以上攻擊程序現(xiàn)在生成如下輸出:

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

  這里有一個簡單的石蕊試法來決定默認(rèn)的 readObject 方法是否可以被類接受:您是否愿意添加一個公共構(gòu)造函數(shù),它將對象中每個非瞬態(tài)字段的值作為參數(shù),并將值存儲在字段中而不進(jìn)行任何驗(yàn)證?如果沒有,則必須提供一個 readObject 方法,它必須執(zhí)行構(gòu)造函數(shù)所需的所有有效性檢查和防御性復(fù)制?;蛘?,您可以使用序列化代理模式(item 90)。強(qiáng)烈推薦使用此模式,因?yàn)樗诎踩葱蛄谢矫婊ㄙM(fèi)了大量精力。
  readObject 方法和應(yīng)用于非最終序列化類的構(gòu)造函數(shù)之間還有一個相似之處。與構(gòu)造函數(shù)一樣,readObject 方法不能直接或間接調(diào)用可重寫的方法(item 19)。如果違反了此規(guī)則,并且覆蓋了所涉及的方法,則覆蓋的方法將在反序列化子類的狀態(tài)之前運(yùn)行。程序失敗很可能導(dǎo)致 Bloch05, Puzzle 91。
  總而言之,在編寫 readObject 方法時,請采用這樣一種思維方式:即編寫公共構(gòu)造函數(shù)時,無論給定的是什么字節(jié)流,都必須生成一個有效實(shí)例。不要假設(shè)字節(jié)流表示一個實(shí)際的序列化實(shí)例。雖然本項(xiàng)目中的示例涉及使用默認(rèn)序列化表單的類,但所引發(fā)的所有問題都同樣適用于使用自定義序列化表單的類。下面是編寫 readObject 方法的指導(dǎo)原則:
? 對于具有必須保持私有的對象引用字段的類,防御性地將每個對象復(fù)制到這樣的字段中。不可變類的可變組件屬于這一類。
? 檢查所有不變量,如果檢查失敗則拋出 InvalidObjectException。檢查應(yīng)該遵循任何防御性復(fù)制。
? 如果在反序列化后必須驗(yàn)證整個對象圖,請使用 ObjectInputValidation 接口(本書未討論)。
? 不要直接或間接調(diào)用類中的任何可重寫方法。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容