在上一講中,我們已經初步接觸了 Java 安全,今天我們將一起探討更多 Java 開發中可能影響到安全的場合。很多安全問題,在特定的上下文,存在著不同的定義,盡管本質是相似或一致的,這是由于 Java 平臺自身的特性所帶來特有的問題。今天這一講我將側重于 Java 開發者的角度談代碼安全,而不是講廣義的安全風險。
今天我要問你的問題是,如何寫出安全的 Java 代碼?
## 典型回答
這個問題可能有點寬泛,我們可以用特定類型的安全風險為例,如拒絕服務(DoS)攻擊,分析 Java 開發者需要重點考慮的點。
DoS 是一種常見的網絡攻擊,有人也稱其為“洪水攻擊”。最常見的表現是,利用大量機器發送請求,將目標網站的帶寬或者其他資源耗盡,導致其無法響應正常用戶的請求。
我認為,從 Java 語言的角度,更加需要重視的是程序級別的攻擊,也就是利用 Java、JVM 或應用程序的瑕疵,進行低成本的 DoS 攻擊,這也是想要寫出安全的 Java 代碼所必須考慮的。例如:
* 如果使用的是早期的 JDK 和 Applet 等技術,攻擊者構建合法但惡劣的程序就相對容易,例如,將其線程優先級設置為最高,做一些看起來無害但空耗資源的事情。幸運的是類似技術已經逐步退出歷史舞臺,在 JDK 9 以后,相關模塊就已經被移除。
* 上一講中提到的哈希碰撞攻擊,就是個典型的例子,對方可以輕易消耗系統有限的 CPU 和線程資源。從這個角度思考,類似加密、解密、圖形處理等計算密集型任務,都要防范被惡意濫用,以免攻擊者通過直接調用或者間接觸發方式,消耗系統資源。
* 利用 Java 構建類似上傳文件或者其他接受輸入的服務,需要對消耗系統內存或存儲的上限有所控制,因為我們不能將系統安全依賴于用戶的合理使用。其中特別注意的是涉及解壓縮功能時,就需要防范[Zip bomb](https://en.wikipedia.org/wiki/Zip_bomb)等特定攻擊。
* 另外,Java 程序中需要明確釋放的資源有很多種,比如文件描述符、數據庫連接,甚至是再入鎖,任何情況下都應該保證資源釋放成功,否則即使平時能夠正常運行,也可能被攻擊者利用而耗盡某類資源,這也算是可能的 DoS 攻擊來源。
所以可以看出,實現安全的 Java 代碼,需要從功能設計到實現細節,都充分考慮可能的安全影響。
## 考點分析
關于今天的問題,以典型的 DoS 攻擊作為切入點,將問題聚焦在 Java 開發中,我介紹了 Java 應用設計、實現的注意事項,后面還會介紹更加全面的實踐。
其實安全問題實際就是軟件的缺陷,軟件安全并不存在一勞永逸的秘籍,既離不開設計、架構中的風險分析,也離不開編碼、測試等階段的安全實踐手段。對于面試官來說,考察安全問題,除了對特定安全領域知識的考察,更多是要看面試者的 Java 編程基本功和知識的積累。
所以,我會在后面會循序漸進探討 Java 安全編程,這里面沒有什么黑科技,只有規范的開發標準,很多安全問題其實是態度問題,取決于你是否真的認真對待它。
* 我將以一些典型的代碼片段為出發點,分析一些非常容易被忽略的安全風險,并介紹安全問題頻發的熱點場景,如 Java 序列化和反序列化。
* 從軟件生命周期的角度,探討設計、開發、測試、部署等不同階段,有哪些常見的安全策略或工具。
## 知識擴展
首先,我們一起來看一段不起眼的條件判斷代碼,這里可能有什么問題嗎?
~~~
// a, b, c 都是 int 類型的數值
if (a + b < c) {
// …
}
~~~
你可能會納悶,這是再常見不過的一個條件判斷了,能有什么安全隱患?
這里的隱患是數值類型需要防范溢出,否則這不僅僅可能會帶來邏輯錯誤,在特定情況下可能導致嚴重的安全漏洞。
從語言特性來說,Java 和 JVM 提供了很多基礎性的改進,相比于傳統的 C、C++ 等語言,對于數組越界等處理要完善的多,原生的避免了[緩沖區溢出](https://en.wikipedia.org/wiki/Buffer_overflow)等攻擊方式,提高了軟件的安全性。但這并不代表完全杜絕了問題,Java 程序可能調用本地代碼,也就是 JNI 技術,錯誤的數值可能導致 C/C++ 層面的數據越界等問題,這是很危險的。
所以,上面的條件判斷,需要判斷其數值范圍,例如,寫成類似下面結構。
~~~
if (a < c – b)
~~~
再來看一個例子,請看下面的一段異常處理代碼:
~~~
try {
// 業務代碼
} catch (Exception e) {
throw new RuntimeException(hostname + port + “ doesn’t response”);
}
~~~
這段代碼將敏感信息包含在異常消息中,試想,如果是一個 Web 應用,異常也沒有良好的包裝起來,很有可能就把內部信息暴露給終端客戶。古人曾經告誡我們“言多必失”是很有道理的,雖然其本意不是指軟件安全,但盡量少暴露信息,也是保證安全的基本原則之一。即使我們并不認為某個信息有安全風險,我的建議也是如果沒有必要,不要暴露出來。
這種暴露還可能通過其他方式發生,比如某著名的編程技術網站,就被曝光過所有用戶名和密碼。這些信息都是明文存儲,傳輸過程也未必進行加密,類似這種情況,暴露只是個時間早晚的問題。
對于安全標準特別高的系統,甚至可能要求敏感信息被使用后,要立即明確在內存中銷毀,以免被探測;或者避免在發生 core dump 時,意外暴露。
第三,Java 提供了序列化等創新的特性,廣泛使用在遠程調用等方面,但也帶來了復雜的安全問題。直到今天,序列化仍然是個安全問題頻發的場景。
針對序列化,通常建議:
* 敏感信息不要被序列化!在編碼中,建議使用 transient 關鍵字將其保護起來。
* 反序列化中,建議在 readObject 中實現與對象構件過程相同的安全檢查和數據檢查。
另外,在 JDK 9 中,Java 引入了過濾器機制,以保證反序列化過程中數據都要經過基本驗證才可以使用。其原理是通過黑名單和白名單,限定安全或者不安全的類型,并且你可以進行定制,然后通過環境變量靈活進行配置, 更加具體的使用你可以參考[ObjectInputFilter](https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.html)。
通過前面的介紹,你可能注意到,很多安全問題都是源于非常基本的編程細節,類似 Immutable、封裝等設計,都存在著安全性的考慮。從實踐的角度,讓每個人都了解和掌握這些原則,有必要但并不太現實,有沒有什么工程實踐手段,可以幫助我們排查安全隱患呢?
**開發和測試階段**
在實際開發中,各種功能點五花八門,未必能考慮的全面。我建議沒有必要所有都需要自己去從頭實現,盡量使用廣泛驗證過的工具、類庫,不管是來自于 JDK 自身,還是 Apache 等第三方組織,都在社區的反饋下持續地完善代碼安全。
開發過程中應用代碼規約標準,是避免安全問題的有效手段。我特別推薦來自孤盡的《阿里巴巴 Java 開發手冊》,以及其配套工具,充分總結了業界在 Java 等領域的實踐經驗,將規約實踐系統性地引入國內的軟件開發,可以有效提高代碼質量。
當然,凡事都是有代價的,規約會增加一定的開發成本,可能對迭代的節奏產生一定影響,所以對于不同階段、不同需求的團隊,可以根據自己的情況對規約進行適應性的調整。
落實到實際開發流程中,以 OpenJDK 團隊為例,我們應用了幾個不同角度的實踐:
* 在早期設計階段,就由安全專家組對新特性進行風險評估。
* 開發過程中,尤其是 code review 階段,應用 OpenJDK 自身定制的代碼規范。
* 利用多種靜態分析工具如[FindBugs](http://findbugs.sourceforge.net/)、[Parfait](https://labs.oracle.com/pls/apex/f?p=labs:49:::::P49_PROJECT_ID:13)等,幫助早期發現潛在安全風險,并對相應問題采取零容忍態度,強制要求解決。
* 甚至 OpenJDK 會默認將任何(編譯等)警告,都當作錯誤對待,并體現在 CI 流程中。
* 在代碼 check-in 等關鍵環節,利用 hook 機制去調用規則檢查工具,以保證不合規代碼不能進入 OpenJDK 代碼庫。
關于靜態分析工具的選擇,我們選取的原則是“足夠好”。沒有什么工具能夠發現所有問題,所以在保證功能的前提下,影響更大的是分析效率,換句話說是代碼分析的噪音高低。不管分析有多么的完備,如果太多誤報,就會導致有用信息被噪音覆蓋,也不利于后續其他程序化的處理,反倒不利于排查問題。
以上這些是為了保證 JDK 作為基礎平臺的苛刻質量要求,在實際產品中,你需要斟酌具體什么程度的要求是合理的。
**部署階段**
JDK 自身的也是個軟件,難免會存在實現瑕疵,我們平時看到 JDK 更新的安全漏洞補丁,其實就是在修補這些漏洞。我最近還注意到,某大廠后臺被曝出了使用的 JDK 版本存在序列化相關的漏洞。類似這種情況,大多數都是因為使用的 JDK 是較低版本,算是可以通過部署解決的問題。
如果是安全敏感型產品,建議關注 JDK 在加解密方面的[路線圖](https://java.com/en/jre-jdk-cryptoroadmap.html),同樣的標準也應用于其他語言和平臺,很多早期認為非常安全的算法,已經被攻破,及時地升級基礎軟件是安全的必要條件。
攻擊和防守是不對稱的,只要有一個嚴重漏洞,對于攻擊者就足夠了,所以,不能對黑盒形式的部署心存僥幸,這并不能保證系統的安全,攻擊者可以利用對軟件設計的猜測,結合一系列手段,探測出漏洞。
今天我以 DoS 等典型攻擊方式為例,分析了其在 Java 平臺上的特定表現,并從更多安全編碼的細節幫你體會安全問題的普遍性,最后我介紹了軟件開發周期中的安全實踐,希望能對你的工作有所幫助。
## 一課一練
關于今天我們討論的題目你做到心中有數了嗎?你在開發中遇到過 Java 特定的安全問題嗎?是怎么解決的呢?
- 前言
- 開篇詞
- 開篇詞 -以面試題為切入點,有效提升你的Java內功
- 模塊一 Java基礎
- 第1講 談談你對Java平臺的理解?
- 第2講 Exception和Error有什么區別?
- 第3講 談談final、finally、 finalize有什么不同?
- 第4講 強引用、軟引用、弱引用、幻象引用有什么區別?
- 第5講 String、StringBuffer、StringBuilder有什么區別?
- 第6講 動態代理是基于什么原理?
- 第7講 int和Integer有什么區別?
- 第8講 對比Vector、ArrayList、LinkedList有何區別?
- 第9講 對比Hashtable、HashMap、TreeMap有什么不同?
- 第10講 如何保證集合是線程安全的? ConcurrentHashMap如何實現高效地線程安全?
- 第11講 Java提供了哪些IO方式? NIO如何實現多路復用?
- 第12講 Java有幾種文件拷貝方式?哪一種最高效?
- 第13講 談談接口和抽象類有什么區別?
- 第14講 談談你知道的設計模式?
- 模塊二 Java進階
- 第15講 synchronized和ReentrantLock有什么區別呢?
- 第16講 synchronized底層如何實現?什么是鎖的升級、降級?
- 第17講 一個線程兩次調用start()方法會出現什么情況?
- 第18講 什么情況下Java程序會產生死鎖?如何定位、修復?
- 第19講 Java并發包提供了哪些并發工具類?
- 第20講 并發包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區別?
- 第21講 Java并發類庫提供的線程池有哪幾種? 分別有什么特點?
- 第22講 AtomicInteger底層實現原理是什么?如何在自己的產品代碼中應用CAS操作?
- 第23講 請介紹類加載過程,什么是雙親委派模型?
- 第24講 有哪些方法可以在運行時動態生成一個Java類?
- 第25講 談談JVM內存區域的劃分,哪些區域可能發生OutOfMemoryError?
- 第26講 如何監控和診斷JVM堆內和堆外內存使用?
- 第27講 Java常見的垃圾收集器有哪些?
- 第28講 談談你的GC調優思路?
- 第29講 Java內存模型中的happen-before是什么?
- 第30講 Java程序運行在Docker等容器環境有哪些新問題?
- 模塊三 Java安全基礎
- 第31講 你了解Java應用開發中的注入攻擊嗎?
- 第32講 如何寫出安全的Java代碼?
- 模塊四 Java性能基礎
- 第33講 后臺服務出現明顯“變慢”,談談你的診斷思路?
- 第34講 有人說“Lambda能讓Java程序慢30倍”,你怎么看?
- 第35講 JVM優化Java代碼時都做了什么?
- 模塊五 Java應用開發擴展
- 第36講 談談MySQL支持的事務隔離級別,以及悲觀鎖和樂觀鎖的原理和應用場景?
- 第37講 談談Spring Bean的生命周期和作用域?
- 第38講 對比Java標準NIO類庫,你知道Netty是如何實現更高性能的嗎?
- 第39講 談談常用的分布式ID的設計方案?Snowflake是否受冬令時切換影響?
- 周末福利
- 周末福利 談談我對Java學習和面試的看法
- 周末福利 一份Java工程師必讀書單
- 結束語
- 結束語 技術沒有終點
- 結課測試 Java核心技術的這些知識,你真的掌握了嗎?