Effective Java(3rd)-Item88 防御性地編寫readObject方法

??項目50包含一個具有可變私有日期字段的不可變?nèi)掌诜秶?。該類通過在構(gòu)造函數(shù)和訪問器中防御性地復制Date對象,不遺余力地保持其不變性和不可變性。類是這樣的:


image.png

??假設您決定讓這個類可序列化。由于句點對象的物理表示精確地反映了它的邏輯數(shù)據(jù)內(nèi)容,所以使用默認的序列化形式并不是不合理的( item87)。因此,要使類可序列化,似乎只需將實現(xiàn)Serializable的單詞添加到類聲明中。但是,如果這樣做,該類將不再保證它的臨界不變量。

??問題是readObject方法實際上是另一個公共構(gòu)造函數(shù),它需要與任何其他構(gòu)造函數(shù)相同的關注。正如構(gòu)造函數(shù)必須檢查其參數(shù)的有效性(item49 )并在適當?shù)牡胤綇椭茀?shù)(item50)一樣,readObject方法也必須這樣做。如果readObject方法沒有做到這兩件事中的任何一件,那么攻擊者就很容易違反類的不變量。

??松散地說,readObject是一個構(gòu)造函數(shù),它唯一的參數(shù)是字節(jié)流。在正常使用中,字節(jié)流是通過序列化一個正常構(gòu)造的實例生成的。當readObject呈現(xiàn)一個字節(jié)流時,問題就出現(xiàn)了,這個字節(jié)流是人為構(gòu)造的,用來生成一個違反類不變量的對象。這樣的字節(jié)流可用于創(chuàng)建一個不可能的對象,而該對象不能使用普通構(gòu)造函數(shù)創(chuàng)建。
??假設我們只是簡單地將Serializable添加到類聲明Period中。然后,這個丑陋的程序?qū)⑸梢粋€Period實例,其結(jié)束先于開始。對字節(jié)值進行強制轉(zhuǎn)換,其高階位被設置,這是由于Java缺乏字節(jié)字面量,并且錯誤地決定對字節(jié)類型進行簽名:


image.png

??用于初始化serializedForm的字節(jié)數(shù)組文本是通過序列化一個普通Period實例并手工編輯得到的字節(jié)流生成的。流的細節(jié)對示例并不重要,但是如果您感興趣,可以在Java對象序列化中描述序列化字節(jié)流格式規(guī)范[序列化,6]。如果您運行這個程序,它將打印1月1日星期五
1999年太平洋夏令時12:00:00 - 1984年1月1日星期日12:00:00。只需聲明Period serializable,就可以創(chuàng)建一個違反其類不變量的對象。
??要解決此問題,請為Period提供一個readObject方法,該方法調(diào)用defaultReadObject,然后檢查反序列化對象的有效性。如果有效性檢查失敗,readObject方法拋出InvalidObjectException,阻止反序列化完成:


image.png

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


image.png

image.png

??要查看攻擊的實際情況,請運行以下程序:


image.png

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


image.png

??雖然創(chuàng)建Period實例時保留了它的不變量,但是可以隨意修改它的內(nèi)部組件。一旦擁有一個可變的Period實例,攻擊者可能會將實例傳遞給一個依賴于Period的不變性來保證其安全性的類,從而造成極大的危害。這并不是牽強附會的:有些類依賴于String的不變性來保證其安全性。
??問題的根源在于Period的readObject方法沒有進行足夠的防御性復制。當對象被反序列化時,防御性地復制任何包含客戶端不能擁有的對象引用的字段是至關重要的。因此,每個包含私有可變組件的可序列化不可變類都必須防御性地在其readObject方法中復制這些組件。下面的readObject方法足以保證周期的不變性,并保持其不變性:

image.png

??注意,防御副本是在有效性檢查之前執(zhí)行的,我們沒有使用Date的clone方法來執(zhí)行防御副本。這兩個細節(jié)都需要保護期間免受攻擊( item50)。還要注意,防御性復制不可能用于final字段。要使用readObject方法,必須使start和end字段非final。這是不幸的,但卻是兩害相權(quán)取其輕。使用新的readObject方法,并從開始和結(jié)束字段中刪除最后的修飾符,MutablePeriod類將無效。上面的攻擊程序現(xiàn)在生成這個輸出:

image.png

??下面是一個簡單的石蕊測試,用于判斷默認的readObject方法是否適合類:您是否愿意添加一個公共構(gòu)造函數(shù),它將對象中每個非瞬態(tài)字段的值作為參數(shù),并在沒有任何驗證的情況下將值存儲在字段中?如果沒有,則必須提供readObject方法,并且它必須執(zhí)行構(gòu)造函數(shù)所需的所有有效性檢查和防御性復制?;蛘撸梢允褂?em>序列化代理模式(item90 )。強烈推薦使用這種模式,因為它會在安全反序列化方面花費大量精力。

??readObject方法和構(gòu)造函數(shù)之間還有一個相似之處,適用于非最終序列化類。與構(gòu)造函數(shù)一樣,readObject方法不能直接或間接調(diào)用可覆蓋的方法(item19)。如果違反了這條規(guī)則,并且涉及的方法被覆蓋,則覆蓋方法將在子類的狀態(tài)反序列化之前運行。程序失敗很可能導致[Bloch05, Puzzle 91]。

??總而言之,無論何時編寫readObject方法,都要采用這樣一種思維方式,即您正在編寫一個公共構(gòu)造函數(shù),該構(gòu)造函數(shù)必須生成一個有效的實例,而不管給定的是什么字節(jié)流。不要假設字節(jié)流表示實際的序列化實例。雖然本項目中的示例涉及使用默認序列化表單的類,但是所引發(fā)的所有問題都同樣適用于具有自定義序列化表單的類。下面是編寫readObject方法的指導原則:

  • 對于具有必須保持私有的對象引用字段的類,防御性地復制該字段中的每個對象。不可變類的可變組件屬于這一類。
  • 檢查任何不變量,如果檢查失敗,則拋出InvalidObjectException。
    檢查應該遵循任何防御性復制。
  • 如果必須在反序列化后驗證整個對象圖,請使用
    ObjectInputValidation接口(在本書中沒有討論)。
  • 不要直接或間接地調(diào)用類中任何可覆蓋的方法。
    ??
    本文寫于2019.7.24,歷時1天
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

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