Tips
書中的源代碼地址:https://github.com/jbloch/effective-java-3e-source-code
注意,書中的有些代碼裏方法是基於Java 9 API中的,因此JDK 最好下載 JDK 9以上的版本。java
本章涉及對象序列化(object serialization),它是Java的框架,用於將對象編碼爲字節流(序列化)並從其編碼中重構對象(反序列化)。 一旦對象被序列化,其編碼能夠從一個虛擬機發送到另外一個虛擬機或存儲在磁盤上以便之後反序列化。 本章重點介紹序列化的風險以及如何將序列化的風險最小化。git
當序列化在1997年添加到Java中時,它被認爲有必定的風險。這種方法曾在研究語言(模塊3)中嘗試過,但從未在生產語言中使用過。雖然程序員不費什麼力氣就能實現分佈式對象的承諾很吸引人,但代價是不可見的構造方法和API與實現之間模糊的界線,可能會出現正確性、性能、安全性和維護方面的問題。支持者認爲收益大於風險,但歷史證實並不是如此。程序員
本書前幾版中描述的安全問題與一些人擔憂的同樣嚴重。 2000年以前中討論的漏洞在將來十年被轉化爲嚴重漏洞,其中最著名的包括2016年11月對舊金山大都會運輸署(San Francisco Metropolitan Transit Agency)市政鐵路(SFMTA Muni)的勒索軟件攻擊,致使整個收費系統關閉了兩天[Gallagher16]。github
序列化的一個基本問題是它的攻擊面太大而沒法保護,並且還在不斷增加:經過調用ObjectInputStream
類上的readObject
方法反序列化對象圖。這個方法本質上是一個神奇的構造方法,能夠用來實例化類路徑上幾乎任何類型的對象,只要該類型實現Serializable接口。在反序列化字節流的過程當中,此方法能夠執行來自任何這些類型的代碼,所以全部這些類型的代碼都是攻擊面的一部分。數組
攻擊面包括Java平臺類庫中的類,第二方類庫(如Apache Commons Collections)和應用程序自己。 即便你遵照全部相關的最佳實踐併成功編寫沒法攻擊的可序列化類,你的應用程序仍可能容易受到攻擊。 引用CERT協調中心技術經理Robert Seacord的話:瀏覽器
Java反序列化是一個明顯且存在的危險,由於它直接被應用程序普遍使用,並間接地由Java子系統(如RMI(遠程方法調用),JMX(Java管理擴展)和JMS(Java消息系統))普遍使用。 不受信任的流的反序列化可能致使遠程代碼執行(RCE),拒絕服務(DoS)以及一系列其餘漏洞利用。 應用程序即便沒有作錯也容易受到這些攻擊。[Seacord17]安全
攻擊者和安全研究人員研究Java類庫和經常使用的第三方類庫中的可序列化類型,尋找在反序列化過程當中調用的執行潛在危險活動的方法。這種方法稱爲gadget。多個gadget能夠同時使用,造成一個gadget鏈(chain)。偶爾會發現gadget鏈,它的功能足夠強大,容許攻擊者在底層硬件上執行任意的本機代碼,只要有機會提交精心設計的字節流進行反序列化。這正是SFMTA Muni襲擊中發生的事情。此次襲擊並非孤立事件。已經發生過,並且還會有更多。服務器
不使用任何gadget,就能夠經過致使須要很長時間反序列化的短字節流,進行反序列化操做,輕鬆地發起拒絕服務攻擊。這種流被稱爲反序列化炸彈(deserialization bombs)[Svoboda16]。下面是Wouter Coekaerts的一個例子,它只使用HashSet和字符串[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個或更少的對象引用。整個流的長度爲5744字節,可是在完成反序列化以前,太陽都已經耗盡了。問題是反序列化HashSet實例須要計算其元素的哈希碼。root
實例的2個元素自己就是包含2個HashSet元素的HashSet,每一個HashSet元素包含2個HashSet元素,以此類推,深度爲100。所以,反序列化set會致使hashCode方法被調用超過2100次。除了反序列化會持續很長時間以外,反序列化器沒有任何錯誤的跡象。生成的對象不多,而且堆棧深度是有界的。分佈式
那麼你能作些什麼來抵禦這些問題呢? 每當反序列化你不信任的字節流時,就會打開攻擊。 避免序列化漏洞利用的最佳方法是永遠不要反序列化任何東西。用1983年電影《戰爭遊戲》(WarGames)中名爲約書亞(Joshua)的電腦的話來講,「惟一的制勝的招式就是不玩」。沒有理由在你編寫的任何新系統中使用Java序列化。 還有其餘在對象和字節序列之間進行轉換的機制,能夠避免Java序列化的許多危險,同時提供許多優點,例如跨平臺支持,高性能,大型工具生態系統以及普遍的專業知識社區。 在本書中,咱們將這些機制稱爲跨平臺結構化數據表示( cross-platform structured-data representations)。 雖然其餘人有時將它們稱爲序列化系統,但本書避免了這種用法,以防止與Java序列化混淆。
這些表示的共同點是它們比Java序列化簡單得多。它們不支持任意對象圖的自動序列化和反序列化。相反,它們支持由一組屬性值對(attribute-value pairs)組成的簡單結構化數據對象。只支持少數基本數據類型和數組數據類型。事實證實,這個簡單的抽象足以構建功能極其強大的分佈式系統,並且足夠簡單,能夠避免Java序列化從一開始就存在的嚴重問題。
領先的跨平臺結構化數據表示是JSON [JSON]和Protocol Buffers,也稱爲protobuf [Protobuf]。 JSON由Douglas Crockford設計用於瀏覽器——服務器通訊,而且Protocol Buffers由Google設計用於在其服務器之間存儲和交換結構化數據。 即便這些表示有時被稱爲中立語言(language-neutral),JSON最初是爲JavaScript開發的,而protobuf是爲C++開發的; 這兩種表述都保留了其起源的痕跡。
JSON和protobuf之間最顯着的區別是JSON是基於文本的,人類可讀的,而protobuf是二進制的,並且效率更高; JSON是一種專門的數據表示,而protobuf提供模式(類型)來文檔記錄和執行適當的用法。 儘管protobuf比JSON更有效,但JSON對於基於文本的表示很是有效。 雖然protobuf是二進制表示,但它確實提供了一種替代文本表示,用於須要人們可讀性的地方(pbtxt)。
若是不能徹底避免Java序列化,可能須要在它的遺留系統環境裏中工做,那麼下一個最佳選擇就是永遠不要反序列化不受信任的數據。特別是,不該該接受來自不可信源的RMI流量。Java的官方安全編碼指南說「反序列化不受信任的數據本質上是危險的,應該避免。這句話是用大號、粗體、斜體和紅色字體設置的,它是整個文檔中惟一應用這種處理([Java-secure)的文本。
若是沒法避免序列化,而且不能絕對肯定反序列化數據的安全性,那麼可使用Java 9中添加的對象反序列化過濾器,並將其移植到早期版本(Java .io. objectinputfilter)。該工具容許指定一個過濾器,該過濾器在反序列化以前應用於數據流。它在類的粒度上運行,容許接受或拒絕某些類。默認接受類並拒絕潛在危險類的列表稱爲黑名單;在默認狀況下拒絕類並接受假定安全的類的列表稱爲白名單。比起黑名單,更喜歡白名單,由於黑名單隻保護你免受已知的威脅。一個名爲Serial Whitelist Application Trainer (SWAT)的工具可用於應用程序自動準備一個白名單[Schneider16]。過濾工具還將保護免受過分使用內存和過深的對象圖的影響,但它不能保護免受如上面所示的序列化炸彈的影響。
不幸的是,序列化在Java生態系統中仍然廣泛存在。 若是要維護基於Java序列化的系統,請認真考慮遷移到跨平臺的結構化數據表示,即便這多是一項耗時的工做。 實際上,可能仍然發現本身必須編寫或維護可序列化的類。 編寫一個正確,安全,高效的可序列化類須要很是當心。 本章的其他部分提供了有關什麼時候以及如何執行此操做的建議。
總之,序列化是危險的,應該避免。若是從頭開始設計一個系統,可使用跨平臺的結構化數據表示,如JSON或protobuf。不要反序列化不受信任的數據。若是必須這樣作,請使用對象反序列化過濾器,但要注意,它不能保證阻止全部攻擊。避免編寫可序列化的類。若是你必須這樣作,必定要很是當心。