ITEM 85: PREFER ALTERNATIVES TO JAVA SERIALIZATION
??1997年將序列化添加到 Java 中時萤晴,人們知道它有一定的風險即舌。這種方法曾在研究語言(Model-3)中試用過,但從未在生產(chǎn)語言中試用過腐芍。雖然程序員只需付出很少的努力就能實現(xiàn)分布式對象的承諾很吸引人初厚,但其代價是不可見的構造函數(shù)和API與實現(xiàn)之間模糊的界限,以及正確性盟迟、性能、安全性和維護方面的潛在問題潦闲。支持者認為收益大于風險攒菠,但歷史證明并非如此。
??在本書的前幾版中描述的安全問題被證明和一些人擔心的一樣嚴重歉闰。21世紀初討論的漏洞在接下來的10年變成了嚴重的漏洞辖众,其中最著名的一次就是在2016年11月對舊金山大都會運輸署市政鐵路(SFMTA Muni)的勒索軟件攻擊卓起,導致整個收費系統(tǒng)關閉了兩天[Gallagher16]。
??序列化的一個基本問題是赵辕,它的攻擊面太大,難以保護概龄,而且還在不斷增長:對象是通過調(diào)用 ObjectInputStream 上的 readObject 方法來反序列化的还惠。這個方法本質(zhì)上是一個神奇的構造函數(shù),可以用來實例化類路徑上幾乎任何類型的對象私杜,只要該類型實現(xiàn) Serializable 接口蚕键。在反序列化字節(jié)流的過程中,此方法可以執(zhí)行這些類型中的任何一種代碼衰粹,因此所有這些類型的代碼都是攻擊表面的一部分锣光。
??攻擊覆蓋了包括 Java 平臺庫、第三方庫(如Apache Commons collection)和應用程序本身中的類铝耻。即使您堅持所有相關的最佳實踐誊爹,并成功編寫了不受攻擊的可序列化類,您的應用程序仍然可能是脆弱的瓢捉。引用 CERT 協(xié)調(diào)中心技術經(jīng)理 Robert Seacord的話:
??“Java反序列化是一個明顯且存在的危險频丘,因為應用程序和 Java 子系統(tǒng)(如RMI(遠程方法調(diào)用)、JMX (Java管理擴展)和JMS (Java消息傳遞系統(tǒng)))都直接或間接地廣泛使用 Java 反序列化泡态。對不受信任的流進行反序列化會導致遠程代碼執(zhí)行(RCE)搂漠、拒絕服務(DoS)和其他一系列攻擊。即使應用程序沒有做錯任何事情某弦,它們也容易受到這些攻擊桐汤。(Seacord17)”
??攻擊者和安全研究人員研究 Java 庫和常用的第三方庫中的可序列化類型,尋找在反序列化期間調(diào)用的執(zhí)行潛在危險活動的方法靶壮。這種方法被稱為 gadget怔毛。多個 gadget可以協(xié)同使用,以形成 gadget 鏈腾降。有時會發(fā)現(xiàn)一個足夠強大的 gadget 鏈馆截,允許攻擊者在底層硬件上執(zhí)行任意本地代碼,只要有機會提交一個精心設計的字節(jié)流進行反序列化蜂莉。這正是在 SFMTA Muni 襲擊中發(fā)生的事情蜡娶。這次襲擊并不是孤立的。曾經(jīng)有過這樣的人映穗,將來還會有更多窖张。
??在不使用任何 gadget 的情況下,您可以通過導致需要很長時間進行反序列化的短流的反序列化蚁滋,輕松地發(fā)起拒絕服務攻擊宿接。這樣的流被稱為反序列化炸彈[Svoboda16]赘淮。這里有一個例子,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
}
??對象由 201 個 HashSet 實例組成睦霎,每個實例包含 3 個或更少的對象引用梢卸。整個流有5,744 字節(jié)長,但是時間在你反序列化它之前就已經(jīng)耗盡了副女。問題是反序列化一個HashSet 實例需要計算其元素的哈希碼蛤高。根哈希集的兩個元素本身就是哈希集,包含2 個哈希集元素碑幅,每個哈希集元素包含 2 個哈希集元素戴陡,以此類推,深度為 100 層沟涨。因此恤批,反序列化該集合會導致 hashCode 方法被調(diào)用超過 2100 次。除了反序列化花費了很長時間這一事實外裹赴,反序列化器沒有指出有什么問題喜庞。產(chǎn)生的對象很少,堆棧深度是有限的棋返。
??那么你能做些什么來抵御這些問題呢赋荆?當您反序列化您不相信的字節(jié)流時,您就會受到攻擊懊昨。避免序列化利用的最好方法是永遠不要反序列化任何東西窄潭。用1983年電影《戰(zhàn)爭游戲》(WarGames)中一臺名叫約書亞(Joshua)的電腦的話來說,“要想取勝酵颁,唯一的辦法就是不去玩嫉你。”沒有理由在你編寫的任何新系統(tǒng)中使用 Java 序列化躏惋。還有其他用于在對象和字節(jié)序列之間轉換的機制幽污,這些機制避免了 Java 序列化的許多危險,同時還提供了許多優(yōu)勢簿姨,例如跨平臺支持距误、高性能、大型工具生態(tài)系統(tǒng)和廣泛的專業(yè)社區(qū)扁位。在本書中准潭,我們將這些機制稱為跨平臺結構化數(shù)據(jù)表示。雖然有些人有時將其稱為序列化系統(tǒng)域仇,但本書避免了這種用法刑然,以免與 Java 序列化混淆。
??這些表示的共同之處在于它們比 Java 序列化要簡單得多暇务。它們不支持任意對象圖的自動序列化和反序列化泼掠。相反怔软,它們支持簡單的、結構化的數(shù)據(jù)對象择镇,這些數(shù)據(jù)對象由一組屬性-值對組成挡逼。只支持少數(shù)基本和數(shù)組數(shù)據(jù)類型。事實證明腻豌,這種簡單的抽象足以構建功能極其強大的分布式系統(tǒng)家坎,也足以簡單地避免自 Java 序列化一開始就困擾它的嚴重問題。
??領先的跨平臺結構化數(shù)據(jù)表示是 JSON [JSON]和協(xié)議緩沖區(qū)饲梭,也稱為 protobuf [protobuf]乘盖。JSON 是由 Douglas Crockford 設計用于瀏覽器-服務器通信的焰檩,而protobuf 是由谷歌設計用于在其服務器之間存儲和交換結構化數(shù)據(jù)憔涉。盡管這些表示有時被稱為語言中立,但 JSON 最初是為 JavaScript 開發(fā)的析苫,而 protobuf 則是為c++ 開發(fā)的;這兩種表象都保留著其起源的痕跡兜叨。
??JSON 和 protobuf 之間最大的區(qū)別是 JSON 是基于文本的,是人類可讀的衩侥,而protobuf 是二進制的国旷,效率更高;JSON 只是一種數(shù)據(jù)表示茫死,而 protobuf 提供模式(類型)來記錄和強制適當?shù)氖褂谩?br>
??盡管 protobuf 比 JSON 更高效跪但,但對于基于文本的表示,JSON 是非常高效的峦萎。雖然 protobuf 是一種二進制表示屡久,但它提供了另一種文本表示,用于需要人類可讀性的地方(pbtxt)爱榔。如果無法完全避免 Java 序列化被环,可能是因為您所工作的遺留系統(tǒng)需要它,那么下一個最好的替代方案是永遠不要反序列化不可信的數(shù)據(jù)详幽。特別是筛欢,永遠不要接受來自不可信來源的RMI流量。Java 的官方安全編碼指南說:“不可信數(shù)據(jù)的反序列化本質(zhì)上是危險的唇聘,應該避免版姑。這個句子被設置為大、粗體迟郎、斜體和紅色漠酿,并且它是整個文檔中唯一得到這種處理的文本[Java-secure]。
??如果不能避免序列化谎亩,并且不能絕對肯定反序列化的數(shù)據(jù)的安全性炒嘲,請使用 Java 9中添加的并向后移植到早期版本的對象反序列化過濾(java.io.ObjectInputFilter)宇姚。此功能允許您指定在反序列化數(shù)據(jù)流之前應用于數(shù)據(jù)流的篩選器。它在類粒度上操作夫凸,允許您接受或拒絕某些類浑劳。默認接受類并拒絕潛在危險的類列表稱為黑名單;默認情況下拒絕類并接受一個假定安全的類列表稱為白名單。選擇白名單而不是黑名單夭拌,因為黑名單只能保護您免受已知的威脅魔熏。一個叫做“連續(xù)白名單應用培訓器”(SWAT)的工具可以用來為你的應用自動準備一個白名單[Schneider16]。過濾功能還可以保護您不受過度內(nèi)存使用和對象圖過于深的影響鸽扁,但它不能保護您不受如上所示的序列化炸彈的影響蒜绽。
??不幸的是,序列化在 Java 生態(tài)系統(tǒng)中仍然很普遍桶现。如果您正在維護一個基于 Java 序列化的系統(tǒng)躲雅,請認真考慮遷移到跨平臺的結構化數(shù)據(jù)表示,盡管這可能是一項耗時的工作骡和。實際上相赁,您可能仍然發(fā)現(xiàn)自己必須編寫或維護一個可序列化的類。編寫正確慰于、安全钮科、有效的可序列化類需要非常小心。本章的其余部分將提供何時以及如何進行此操作的建議婆赠。
??總之绵脯,序列化是危險的,應該避免休里。如果您從頭開始設計系統(tǒng)蛆挫,請使用跨平臺的結構化數(shù)據(jù)表示,如 JSON 或 protobuf份帐。不要反序列化不受信任的數(shù)據(jù)璃吧。如果必須這樣做,請使用對象反序列化過濾废境,但請注意畜挨,它不能保證阻止所有攻擊。避免編寫可序列化的類噩凹。如果你必須這樣做巴元,要非常謹慎。