??項目50包含一個具有可變私有日期字段的不可變日期范圍類。該類通過在構造函數和訪問器中防御性地復制Date對象,不遺余力地保持其不變性和不可變性。類是這樣的:
??假設您決定讓這個類可序列化。由于句點對象的物理表示精確地反映了它的邏輯數據內容,所以使用默認的序列化形式并不是不合理的( item87)。因此,要使類可序列化,似乎只需將實現Serializable的單詞添加到類聲明中。但是,如果這樣做,該類將不再保證它的臨界不變量。
??問題是readObject方法實際上是另一個公共構造函數,它需要與任何其他構造函數相同的關注。正如構造函數必須檢查其參數的有效性(item49 )并在適當的地方復制參數(item50)一樣,readObject方法也必須這樣做。如果readObject方法沒有做到這兩件事中的任何一件,那么攻擊者就很容易違反類的不變量。
??松散地說,readObject是一個構造函數,它唯一的參數是字節流。在正常使用中,字節流是通過序列化一個正常構造的實例生成的。當readObject呈現一個字節流時,問題就出現了,這個字節流是人為構造的,用來生成一個違反類不變量的對象。這樣的字節流可用于創建一個不可能的對象,而該對象不能使用普通構造函數創建。
??假設我們只是簡單地將Serializable添加到類聲明Period中。然后,這個丑陋的程序將生成一個Period實例,其結束先于開始。對字節值進行強制轉換,其高階位被設置,這是由于Java缺乏字節字面量,并且錯誤地決定對字節類型進行簽名:
??用于初始化serializedForm的字節數組文本是通過序列化一個普通Period實例并手工編輯得到的字節流生成的。流的細節對示例并不重要,但是如果您感興趣,可以在Java對象序列化中描述序列化字節流格式規范[序列化,6]。如果您運行這個程序,它將打印1月1日星期五
1999年太平洋夏令時12:00:00 - 1984年1月1日星期日12:00:00。只需聲明Period serializable,就可以創建一個違反其類不變量的對象。
??要解決此問題,請為Period提供一個readObject方法,該方法調用defaultReadObject,然后檢查反序列化對象的有效性。如果有效性檢查失敗,readObject方法拋出InvalidObjectException,阻止反序列化完成:
??雖然這可以防止攻擊者創建無效的Period實例,但還有一個更微妙的問題仍然潛伏著。可以通過創建一個字節流來創建一個可變的句點實例,該字節流以一個有效的Period實例開始,然后向句點實例內部的私有日期字段追加額外的引用。攻擊者從ObjectInputStream中讀取Period實例,然后讀取附加到流中的“流氓對象引用”。這些引用使攻擊者能夠訪問私有對象引用的對象
句點對象中的日期字段。通過修改這些日期實例,攻擊者可以修改Period實例。下面的類演示了這種攻擊:
??要查看攻擊的實際情況,請運行以下程序:
??在我的語言環境中,運行這個程序會產生以下輸出:
??雖然創建Period實例時保留了它的不變量,但是可以隨意修改它的內部組件。一旦擁有一個可變的Period實例,攻擊者可能會將實例傳遞給一個依賴于Period的不變性來保證其安全性的類,從而造成極大的危害。這并不是牽強附會的:有些類依賴于String的不變性來保證其安全性。
??問題的根源在于Period的readObject方法沒有進行足夠的防御性復制。當對象被反序列化時,防御性地復制任何包含客戶端不能擁有的對象引用的字段是至關重要的。因此,每個包含私有可變組件的可序列化不可變類都必須防御性地在其readObject方法中復制這些組件。下面的readObject方法足以保證周期的不變性,并保持其不變性:
??注意,防御副本是在有效性檢查之前執行的,我們沒有使用Date的clone方法來執行防御副本。這兩個細節都需要保護期間免受攻擊( item50)。還要注意,防御性復制不可能用于final字段。要使用readObject方法,必須使start和end字段非final。這是不幸的,但卻是兩害相權取其輕。使用新的readObject方法,并從開始和結束字段中刪除最后的修飾符,MutablePeriod類將無效。上面的攻擊程序現在生成這個輸出:
??下面是一個簡單的石蕊測試,用于判斷默認的readObject方法是否適合類:您是否愿意添加一個公共構造函數,它將對象中每個非瞬態字段的值作為參數,并在沒有任何驗證的情況下將值存儲在字段中?如果沒有,則必須提供readObject方法,并且它必須執行構造函數所需的所有有效性檢查和防御性復制。或者,您可以使用序列化代理模式(item90 )。強烈推薦使用這種模式,因為它會在安全反序列化方面花費大量精力。
??readObject方法和構造函數之間還有一個相似之處,適用于非最終序列化類。與構造函數一樣,readObject方法不能直接或間接調用可覆蓋的方法(item19)。如果違反了這條規則,并且涉及的方法被覆蓋,則覆蓋方法將在子類的狀態反序列化之前運行。程序失敗很可能導致[Bloch05, Puzzle 91]。
??總而言之,無論何時編寫readObject方法,都要采用這樣一種思維方式,即您正在編寫一個公共構造函數,該構造函數必須生成一個有效的實例,而不管給定的是什么字節流。不要假設字節流表示實際的序列化實例。雖然本項目中的示例涉及使用默認序列化表單的類,但是所引發的所有問題都同樣適用于具有自定義序列化表單的類。下面是編寫readObject方法的指導原則:
- 對于具有必須保持私有的對象引用字段的類,防御性地復制該字段中的每個對象。不可變類的可變組件屬于這一類。
- 檢查任何不變量,如果檢查失敗,則拋出InvalidObjectException。
檢查應該遵循任何防御性復制。 - 如果必須在反序列化后驗證整個對象圖,請使用
ObjectInputValidation接口(在本書中沒有討論)。 - 不要直接或間接地調用類中任何可覆蓋的方法。
??
本文寫于2019.7.24,歷時1天