ITEM 85: PREFER ALTERNATIVES TO JAVA SERIALIZATION
??1997年將序列化添加到 Java 中時(shí),人們知道它有一定的風(fēng)險(xiǎn)。這種方法曾在研究語(yǔ)言(Model-3)中試用過,但從未在生產(chǎn)語(yǔ)言中試用過。雖然程序員只需付出很少的努力就能實(shí)現(xiàn)分布式對(duì)象的承諾很吸引人,但其代價(jià)是不可見的構(gòu)造函數(shù)和API與實(shí)現(xiàn)之間模糊的界限,以及正確性、性能、安全性和維護(hù)方面的潛在問題。支持者認(rèn)為收益大于風(fēng)險(xiǎn),但歷史證明并非如此。
??在本書的前幾版中描述的安全問題被證明和一些人擔(dān)心的一樣嚴(yán)重。21世紀(jì)初討論的漏洞在接下來的10年變成了嚴(yán)重的漏洞,其中最著名的一次就是在2016年11月對(duì)舊金山大都會(huì)運(yùn)輸署市政鐵路(SFMTA Muni)的勒索軟件攻擊,導(dǎo)致整個(gè)收費(fèi)系統(tǒng)關(guān)閉了兩天[Gallagher16]。
??序列化的一個(gè)基本問題是,它的攻擊面太大,難以保護(hù),而且還在不斷增長(zhǎng):對(duì)象是通過調(diào)用 ObjectInputStream 上的 readObject 方法來反序列化的。這個(gè)方法本質(zhì)上是一個(gè)神奇的構(gòu)造函數(shù),可以用來實(shí)例化類路徑上幾乎任何類型的對(duì)象,只要該類型實(shí)現(xiàn) Serializable 接口。在反序列化字節(jié)流的過程中,此方法可以執(zhí)行這些類型中的任何一種代碼,因此所有這些類型的代碼都是攻擊表面的一部分。
??攻擊覆蓋了包括 Java 平臺(tái)庫(kù)、第三方庫(kù)(如Apache Commons collection)和應(yīng)用程序本身中的類。即使您堅(jiān)持所有相關(guān)的最佳實(shí)踐,并成功編寫了不受攻擊的可序列化類,您的應(yīng)用程序仍然可能是脆弱的。引用 CERT 協(xié)調(diào)中心技術(shù)經(jīng)理 Robert Seacord的話:
??“Java反序列化是一個(gè)明顯且存在的危險(xiǎn),因?yàn)閼?yīng)用程序和 Java 子系統(tǒng)(如RMI(遠(yuǎn)程方法調(diào)用)、JMX (Java管理擴(kuò)展)和JMS (Java消息傳遞系統(tǒng)))都直接或間接地廣泛使用 Java 反序列化。對(duì)不受信任的流進(jìn)行反序列化會(huì)導(dǎo)致遠(yuǎn)程代碼執(zhí)行(RCE)、拒絕服務(wù)(DoS)和其他一系列攻擊。即使應(yīng)用程序沒有做錯(cuò)任何事情,它們也容易受到這些攻擊。(Seacord17)”
??攻擊者和安全研究人員研究 Java 庫(kù)和常用的第三方庫(kù)中的可序列化類型,尋找在反序列化期間調(diào)用的執(zhí)行潛在危險(xiǎn)活動(dòng)的方法。這種方法被稱為 gadget。多個(gè) gadget可以協(xié)同使用,以形成 gadget 鏈。有時(shí)會(huì)發(fā)現(xiàn)一個(gè)足夠強(qiáng)大的 gadget 鏈,允許攻擊者在底層硬件上執(zhí)行任意本地代碼,只要有機(jī)會(huì)提交一個(gè)精心設(shè)計(jì)的字節(jié)流進(jìn)行反序列化。這正是在 SFMTA Muni 襲擊中發(fā)生的事情。這次襲擊并不是孤立的。曾經(jīng)有過這樣的人,將來還會(huì)有更多。
??在不使用任何 gadget 的情況下,您可以通過導(dǎo)致需要很長(zhǎng)時(shí)間進(jìn)行反序列化的短流的反序列化,輕松地發(fā)起拒絕服務(wù)攻擊。這樣的流被稱為反序列化炸彈[Svoboda16]。這里有一個(gè)例子,Wouter Coekaerts只使用哈希集和字符串[Coekaerts15]:
// Deserialization bomb - deserializing this stream takes forever
static byte[] bomb() {
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1);
s1.add(t2);
s2.add(t1);
s2.add(t2);
s1 = t1;
s2 = t2;
}
return serialize(root); // Method omitted for brevity
}
??對(duì)象由 201 個(gè) HashSet 實(shí)例組成,每個(gè)實(shí)例包含 3 個(gè)或更少的對(duì)象引用。整個(gè)流有5,744 字節(jié)長(zhǎng),但是時(shí)間在你反序列化它之前就已經(jīng)耗盡了。問題是反序列化一個(gè)HashSet 實(shí)例需要計(jì)算其元素的哈希碼。根哈希集的兩個(gè)元素本身就是哈希集,包含2 個(gè)哈希集元素,每個(gè)哈希集元素包含 2 個(gè)哈希集元素,以此類推,深度為 100 層。因此,反序列化該集合會(huì)導(dǎo)致 hashCode 方法被調(diào)用超過 2100 次。除了反序列化花費(fèi)了很長(zhǎng)時(shí)間這一事實(shí)外,反序列化器沒有指出有什么問題。產(chǎn)生的對(duì)象很少,堆棧深度是有限的。
??那么你能做些什么來抵御這些問題呢?當(dāng)您反序列化您不相信的字節(jié)流時(shí),您就會(huì)受到攻擊。避免序列化利用的最好方法是永遠(yuǎn)不要反序列化任何東西。用1983年電影《戰(zhàn)爭(zhēng)游戲》(WarGames)中一臺(tái)名叫約書亞(Joshua)的電腦的話來說,“要想取勝,唯一的辦法就是不去玩?!睕]有理由在你編寫的任何新系統(tǒng)中使用 Java 序列化。還有其他用于在對(duì)象和字節(jié)序列之間轉(zhuǎn)換的機(jī)制,這些機(jī)制避免了 Java 序列化的許多危險(xiǎn),同時(shí)還提供了許多優(yōu)勢(shì),例如跨平臺(tái)支持、高性能、大型工具生態(tài)系統(tǒng)和廣泛的專業(yè)社區(qū)。在本書中,我們將這些機(jī)制稱為跨平臺(tái)結(jié)構(gòu)化數(shù)據(jù)表示。雖然有些人有時(shí)將其稱為序列化系統(tǒng),但本書避免了這種用法,以免與 Java 序列化混淆。
??這些表示的共同之處在于它們比 Java 序列化要簡(jiǎn)單得多。它們不支持任意對(duì)象圖的自動(dòng)序列化和反序列化。相反,它們支持簡(jiǎn)單的、結(jié)構(gòu)化的數(shù)據(jù)對(duì)象,這些數(shù)據(jù)對(duì)象由一組屬性-值對(duì)組成。只支持少數(shù)基本和數(shù)組數(shù)據(jù)類型。事實(shí)證明,這種簡(jiǎn)單的抽象足以構(gòu)建功能極其強(qiáng)大的分布式系統(tǒng),也足以簡(jiǎn)單地避免自 Java 序列化一開始就困擾它的嚴(yán)重問題。
??領(lǐng)先的跨平臺(tái)結(jié)構(gòu)化數(shù)據(jù)表示是 JSON [JSON]和協(xié)議緩沖區(qū),也稱為 protobuf [protobuf]。JSON 是由 Douglas Crockford 設(shè)計(jì)用于瀏覽器-服務(wù)器通信的,而protobuf 是由谷歌設(shè)計(jì)用于在其服務(wù)器之間存儲(chǔ)和交換結(jié)構(gòu)化數(shù)據(jù)。盡管這些表示有時(shí)被稱為語(yǔ)言中立,但 JSON 最初是為 JavaScript 開發(fā)的,而 protobuf 則是為c++ 開發(fā)的;這兩種表象都保留著其起源的痕跡。
??JSON 和 protobuf 之間最大的區(qū)別是 JSON 是基于文本的,是人類可讀的,而protobuf 是二進(jìn)制的,效率更高;JSON 只是一種數(shù)據(jù)表示,而 protobuf 提供模式(類型)來記錄和強(qiáng)制適當(dāng)?shù)氖褂谩?br>
??盡管 protobuf 比 JSON 更高效,但對(duì)于基于文本的表示,JSON 是非常高效的。雖然 protobuf 是一種二進(jìn)制表示,但它提供了另一種文本表示,用于需要人類可讀性的地方(pbtxt)。如果無法完全避免 Java 序列化,可能是因?yàn)槟ぷ鞯倪z留系統(tǒng)需要它,那么下一個(gè)最好的替代方案是永遠(yuǎn)不要反序列化不可信的數(shù)據(jù)。特別是,永遠(yuǎn)不要接受來自不可信來源的RMI流量。Java 的官方安全編碼指南說:“不可信數(shù)據(jù)的反序列化本質(zhì)上是危險(xiǎn)的,應(yīng)該避免。這個(gè)句子被設(shè)置為大、粗體、斜體和紅色,并且它是整個(gè)文檔中唯一得到這種處理的文本[Java-secure]。
??如果不能避免序列化,并且不能絕對(duì)肯定反序列化的數(shù)據(jù)的安全性,請(qǐng)使用 Java 9中添加的并向后移植到早期版本的對(duì)象反序列化過濾(java.io.ObjectInputFilter)。此功能允許您指定在反序列化數(shù)據(jù)流之前應(yīng)用于數(shù)據(jù)流的篩選器。它在類粒度上操作,允許您接受或拒絕某些類。默認(rèn)接受類并拒絕潛在危險(xiǎn)的類列表稱為黑名單;默認(rèn)情況下拒絕類并接受一個(gè)假定安全的類列表稱為白名單。選擇白名單而不是黑名單,因?yàn)楹诿麊沃荒鼙Wo(hù)您免受已知的威脅。一個(gè)叫做“連續(xù)白名單應(yīng)用培訓(xùn)器”(SWAT)的工具可以用來為你的應(yīng)用自動(dòng)準(zhǔn)備一個(gè)白名單[Schneider16]。過濾功能還可以保護(hù)您不受過度內(nèi)存使用和對(duì)象圖過于深的影響,但它不能保護(hù)您不受如上所示的序列化炸彈的影響。
??不幸的是,序列化在 Java 生態(tài)系統(tǒng)中仍然很普遍。如果您正在維護(hù)一個(gè)基于 Java 序列化的系統(tǒng),請(qǐng)認(rèn)真考慮遷移到跨平臺(tái)的結(jié)構(gòu)化數(shù)據(jù)表示,盡管這可能是一項(xiàng)耗時(shí)的工作。實(shí)際上,您可能仍然發(fā)現(xiàn)自己必須編寫或維護(hù)一個(gè)可序列化的類。編寫正確、安全、有效的可序列化類需要非常小心。本章的其余部分將提供何時(shí)以及如何進(jìn)行此操作的建議。
??總之,序列化是危險(xiǎn)的,應(yīng)該避免。如果您從頭開始設(shè)計(jì)系統(tǒng),請(qǐng)使用跨平臺(tái)的結(jié)構(gòu)化數(shù)據(jù)表示,如 JSON 或 protobuf。不要反序列化不受信任的數(shù)據(jù)。如果必須這樣做,請(qǐng)使用對(duì)象反序列化過濾,但請(qǐng)注意,它不能保證阻止所有攻擊。避免編寫可序列化的類。如果你必須這樣做,要非常謹(jǐn)慎。