JDK 24 功能:JEP 486 Java Security Manager 安全管理器的最終章

JEP 486: Permanently Disable the Security Manager

作為 Java 平台的元老級功能之一,Security Manager 安全管理器自 Java 誕生之初就扮演著程式碼安全守門員的角色。然而,隨著時代演進,它的存在價值已經逐漸消失。

因此,JDK 24JEP 486 中預計徹底移除安全管理器,並為 Java 開發帶來更簡潔且更現代化的安全機制。本文將深入探討為何要移除安全管理器,以及分析其對於 Java 開發生態系統的影響。

前言

這些年以來,Security Manager 安全管理器已經不再是保護客戶端 Java 程式碼的主要手段。而且,它在伺服器端的應用也極為罕見,並且維護成本高昂。

SecurityManager 的設計理念是最低權限原則(principle of least privilege),意即預設讓程式碼處於不受信任的狀態,並且需要明確授予權限後才能存取系統資源。理論上,它可以有效防範惡意程式碼或意外漏洞,但實際執行時卻面臨許多挑戰。

首先是權限管理機制過於複雜,導致了大多數開發人員完全停用安全管理器,或是乾脆給予全部權限,使得安全管理形同虛設。更糟糕的是,為了維持安全管理器的運作,Java 平台函式庫必須將權限檢查的程式碼寫進一千個以上的方法之中。這不但增加了程式碼的複雜度,也帶來了巨大的維護成本。

根據統計數據顯示,自 Java 17 將 SecurityManager 標記為棄用以來,Java 社群中幾乎沒有出現相關的討論或反彈。許多知名工具都已移除了對它的支援,顯示安全管理器在現代 Java 應用程式開發中已失去其重要性。

安全管理器的複雜與沒落

最低權限模型在 Java 函式庫中引入了非比尋常的複雜度,無論是網路、I/O 和 JDBC,一直到 XML、AWT 與 Swing 都必須實作此一權限模型,使得安全管理器被啟用時會產生預期的作用(預設為停用):

  • 當安全管理器啟用時,超過 1,000 個方法必須檢查資源的存取權限。例如,FileOutputStream建構子委派給安全管理器,並使用複雜的演算法來決定是否允許程式存取某資源
  • 當安全管理器啟用時,超過 1,200 個方法必須要提升權限。例如某程式沒有讀取檔案的權限,但它呼叫了 java.time.LocalDateTime.now(),那麼 java.time 的程式碼就必須取得更高的權限,以便讀取 JDK 內部的時區資料庫檔

OpenJDK 核心函式庫小組(OpenJDK Core Libraries Group)已投入了大量的時間和精力去審查上述方法中的每一次變更。在設計每個新的 API 時都必須要考慮到最低權限模型,並且必須仔細查核它們的實作程式碼。

可惜的是,實際上只有極少數的應用程式會啟用安全管理器。尤其大多數程式會盲目地授予所有權限,從而放棄了最低權限模型的好處。因此在 Java 17 時,JEP 411 標記安全管理器為棄用,並預計在之後的版本中移除。

除了棄用安全管理器和相關 API 之外,JDK 也修改成當啟用安全管理器時會發出警告訊息。這些變更讓使用者和開發人員可以預先做好準備,以便在未來版本中能完全地移除安全管理器。

棄用安全管理器的影響

隨著開發人員和企業逐步從 JDK 8 和 11 升級,讓 JDK 17 與之後的版本獲得了廣泛使用。那麼當大家逐步升級後,棄用安全管理器是否對既有或新開發的程式帶來任何影響嗎?

在調查之後發現,Java 生態系中幾乎沒有看到有關啟用安全管理器時發出警告的討論。這代表安全管理器對於 Java 開發人員來說,幾乎完全無關緊要。因此 JEP 411 中的說法似乎是正確的:

「自安全管理器推出以來的四分之一個世紀裡,其採用率一直很低」,

「總之,開發人員對於使用安全管理器來開發現代 Java 應用程式一事,並沒有顯著的興趣。」

自 JDK 17 發布以來,一些少數支援安全管理器的框架和工具的維護者已經移除了對它的支援;這些包括 DerbyAntSpotBugsTomcat。Jakarta EE 的維護者也移除了 EE 應用程式必須支援安全管理器的要求。另外,目前也沒有任何新的專案支援安全管理器。

未來的計畫

絕大多數的應用程式、函式庫和工具已不再需要且也不建議使用安全管理器。如果有程式碼使用了安全管理器,它們也無法運作。因此 Java 生態系得要邁出下一步,開始全面停止使用安全管理器。

接下來,安全管理器的規範會被修改,使得開發人員無法啟用它,並且其他的 Java 函式庫也不會再使用它來做資源存取的決策審查。短期之內,平台仍會保留一個最小版本的 java.lang.SecurityManager 類別,藉以保持和少數仍在使用它的程式、函式庫與工具的相容性。最終在一段時間後,於未來的版本中移除此類別。

移除安全管理器後的安全性之路

團隊相信大多數 Java 開發人員更希望平台中實作那些以網路為主的程式中所需要的安全功能。因此,在移除了複雜的安全管理器之後,核心函式庫小組成員們可以一併移除 JDK 中有關安全管理器的實作,以及數千個權限檢查和權限提升的程式碼,讓貢獻者們投注更多的時間和精力在其他工作上,像是:

  • 實作新協定,例如 TLS 1.3 和 HTTP/3
  • 實作更現代、更強健的加密演算法,例如 HSS/LMS、SHA-3、RSASSA-PSS 和 EdDSA
  • 棄用並禁用弱加密協定和演算法
  • 引入用於金鑰封裝和金鑰衍生的加密 API,為後量子加密演算法提供基礎支援

現今大部分的資安威脅皆與惡意資料有關,而安全管理器難以有效防禦這類威脅。移除了安全管理器之後,我們就可以將重心放在直接防禦惡意資料的安全功能上,例如:

  • 更安全的序列化: 在反序列化的解析時可能會遇到帶有惡意的資料。Java 9 引進了反序列化過濾器,可以從一開始就阻止惡意資料被反序列化。目前也同時在尋求更好的序列化方法
  • 更嚴格的 XML 處理:XML 文件可以參照網路上任何地方的文件類型定義(DTDs),導致 JDK 可能會開啟不受信任的網路連線。在 JDK 23 中,開發人員可以用 java -Djava.xml.config.file=... 去指定一個禁止對外連線的設定檔

程式碼沙箱

沙箱是指讓某些程式碼能夠在受限制的權限與環境下執行。例如,我們會在沙箱中執行那些不受信任或可能具有敵意的程式碼,以避免實際執行它們時造成損害。

在過往歷史中,雖然安全管理器曾經被用來做為 Java Applet 的沙箱,但是並不適合做為整個應用程式的沙箱。Java 程式應該要和其他原生程式一樣,使用 JDK 之外的技術來建立沙箱,例如容器虛擬機器監視器,或是作業系統機制,像是 macOS 的 App Sandbox 或 Linux 的 seccomp

如同安全管理器一樣,上述技術可以限制程式使用本地和遠端資源的方式,例如它們可以防止程式碼存取網路以竊取資料。與安全管理器不同的是,這些技術已被廣泛採用,並且相對易於學習和有效使用。

攔截 API 呼叫

少數程式會用安全管理器去攔截 API 呼叫。然而,真正惡意的程式碼有數不清的方法可以繞過安全管理器對 API 呼叫的攔截。儘管如此,一些應用程式發現攔截很有用,特別是用於阻擋對 System::exit 等方法的呼叫。

在大多數情況下,那些看似需要攔截的問題,可以透過 JDK 之外的技術充分解決。例如,原始碼修改、靜態程式碼分析與重寫,或是在類別載入時基於代理的動態程式碼重寫。

JEP 486 概觀

作為移除安全管理器的下一步,JEP 486 修訂了 Java 平台規範,讓開發者無法啟用它,並且也移除平台中對它的任何引用。此變更對絕大多數的應用程式、函式庫和工具都不會產生影響。

目標

  • 移除在執行 Java 時啟用安全管理器的能力(java -Djava.security.manager ...
  • 移除在程式執行期間安裝安全管理器的能力(System.setSecurityManager(...)
  • 改善目前使用了安全管理器進行資源存取決策的數百個類別的可維護性
  • 修訂安全管理器 API 的規範,使其所有實作行為都如同安全管理器從未被啟用過
  • 在此版本中仍會保留安全管理器 API,讓相依於它的現有程式碼能夠有時間進行遷移

優點

  • 簡化 Java 平台的核心程式碼
  • 降低維護成本
  • 釋放開發資源,專注於更現代的安全機制
  • 提升程式碼的可讀性和可維護性

缺點

  • 可能影響少數仍然依賴安全管理器的舊系統
  • 需要尋找替代方案來實現程式碼沙箱化

介紹

啟用安全管理器會拋出錯誤

在 JDK 24 中,如果我們啟用安全管理器,或是在執行期間安裝自訂安全管理器的話,程式會發出錯誤訊息。例如下例指令:

$ java -Djava.security.manager                  -jar app.jar
$ java -Djava.security.manager=""               -jar app.jar
$ java -Djava.security.manager=allow            -jar app.jar
$ java -Djava.security.manager=default          -jar app.jar
$ java -Djava.security.manager=com.foo.CustomSM -jar app.jar

會導致 JVM 回報錯誤並退出程式:

Error occurred during initialization of VM
java.lang.Error: A command line option has attempted to allow or enable the Security Manager. Enabling a Security Manager is not supported.
        at java.lang.System.initPhase3(java.base@24/System.java:2067)

此錯誤訊息無法被抑制,也無法降級為 JDK 17 到 23 版本中所發出的警告

如果我們在執行期間明確停用自訂安全管理器的話,則不會有任何錯誤產生,例如:

$ java -jar app.jar
$ java -Djava.security.manager=disallow -jar app.jar

此時不會發出任何警告或錯誤訊息,程式會在沒有安全管理器的情況下執行,如同之前一樣。因為自 JDK 18 起,java.security.manager預設值已經是 disallow,所以上述兩行指令的意義是一樣的。

另外,在執行期間呼叫 System::setSecurityManager 安裝安全管理器的話,JVM 會拋出 UnsupportedOperationException

Setting a Security Manager is not supported

如何判斷是否啟用了安全管理器

如果不確定程式是否有啟用安全管理器的話,可以用以下幾個方法找出答案:

  • 檢查腳本或文件,確認程式是否透過命令列選項允許或啟用安全管理器,或者是否需要安裝和設定安全策略檔案
  • 在 JDK 17 到 23 的任一版本上執行程式,並留意主控台上是否有警告訊息,指出安全管理器已被棄用並將在未來的版本中移除
  • 在 JDK 17 到 23 的任一版本上,使用命令列選項 Djava.security.manager=disallow 來執行程式。如果程式中呼叫 System::setSecurityManager 方法安裝自訂安全管理器,那麼 JVM 將拋出 UnsupportedOperationException
  • 用 JDK 17 到 23 中的 jdeprscan 工具來掃描程式碼,找出是否使用了已棄用的安全管理器 API,例如 System::setSecurityManagerjava.security.Policy::setPolicy

安全管理器 API 將會失效

安全管理器 API 包含以下內容:

  • java.lang.SecurityManager 類別中的方法
  • java.lang.System 類別中的 getSecurityManagersetSecurityManager 方法
  • java.security 套件中的 AccessControllerAccessControlContextPolicyProtectionDomain 類別中的方法

JEP 486 不會移除這些方法,而是改變它們的行為,使其無法正常運作。這些方法將視情況返回 nullfalse,或是無條件拋出 SecurityExceptionUnsupportedOperationException

雖然這些行為變更對於大多數使用安全管理器 API 的函式庫來說是相容的,但是有極少數函式庫會產生副作用,詳情請參閱函式庫維護者的建議。完整的行為變更列表請參閱這裡

除了修改 API 行為之外,還會發生以下的變更:

其他 API 的變更

Java 平台中大約有 1,000 個建構子與方法,會在安全管理器啟用且未授予適當權限時拋出 SecurityException。這些方法涵蓋 264 個類別、73 個套件和 25 個模組。例如在 java.base 模組中,就有 640 個方法會拋出 SecurityException

JEP 486 修訂了上述建構子與方法的規格,並移除 SecurityException 的描述,因為該例外已經永遠不會再被拋出。舉例來說,java.io.FileOutputStream 建構子 JavaDoc 的範例變更(刪除的內容以刪除線表示):

public FileOutputStream(String name)
         throws FileNotFoundException

建立一個檔案輸出串流來寫入指定名稱的檔案。系統將建立一個新的 FileDescriptor 物件來表示此檔案連線。

首先,如果有安全管理員的話,則會呼叫其 checkWrite 方法,並將 name 作為參數。

如果該檔案存在但為目錄而非普通檔案,或是檔案不存在但無法建立,或者因其他原因無法開啟,則會拋出 FileNotFoundException。

實作要求 (Implementation Requirements):
    呼叫此建構子並傳入 name 參數等同於呼叫 new FileOutputStream(name, false)。

參數 (Parameters):
    name - 依據系統的檔案名稱。

拋出例外 (Throws):
    FileNotFoundException - 若檔案存在但為目錄、檔案不存在但無法建立,或因其他原因無法開啟時拋出。
    SecurityException - 若安全管理員存在且其 checkWrite 方法拒絕對該檔案的寫入存取時拋出。

參閱 (See Also):
    SecurityManager.checkWrite(java.lang.String)

此變更確保 SecurityException 不再出現於 API 規格中,反映出安全管理器已完全失效。

對於支援安全管理器的函式庫維護者的建議

少數函式庫被設計成在安全管理器啟用時會使用它。這些函式庫通常採用兩種慣用法:

  1. 呼叫 System::getSecurityManager 檢查安全管理器是否已啟用
    • 如果是,則呼叫 SecurityManager::checkPermission 檢查某個操作應被授予或拒絕
      SecurityManager sm = System.getSecurityManager();
      if (sm != null) { sm.checkPermission(...); }
  2. 呼叫 AccessController::doPrivileged 以不同於呼叫端程式碼的權限來執行程式碼
    SomeReturnValue v = AccessController.doPrivileged(() -> { ... return theResult; });

在 JDK 24 中,由於安全管理器永不啟用,System::getSecurityManagerAccessController::doPrivileged 方法的行為與 JDK 17 中未啟用安全管理器時相同:

  • System::getSecurityManager 會返回 null,且
  • 六個 AccessController::doPrivileged 方法會立即執行指定的動作

因此,呼叫這些方法的少數函式庫無需修改即可在 JDK 24 上執行。然而,未來版本中它們將會被移除,因此仍需要儘早更新程式碼。

極少數函式庫使用安全管理器 API 的進階部分來實作自訂的執行環境。例如,某個函式庫可能會呼叫 AccessController::checkPermission 來強制執行自己的權限模型,或者呼叫 Policy::setPolicy 讓自訂的安全管理器將某些資源視為禁止存取。

在 JDK 24 中,這些方法總是實作一個不允許存取所有資源的執行環境。因此,這些方法的行為與它們在 JDK 17 中的行為不同:

  • AccessController::checkPermission 總是拋出 AccessControlException
  • Policy::setPolicy 總是拋出 UnsupportedOperationException,且
  • SecurityManager::check* 方法總是拋出 SecurityException

更多關於 JEP 486 安全管理器 API 行為變更的完整資訊,請參閱此處

總結

JEP 486 Security Manager 安全管理器的退場象徵著 Java 平台朝向更現代化、更實用的安全機制邁進。這項變革雖然可能對某些遺留系統造成短期影響,但從長遠來看,這將有助於 Java 平台更有效地因應現代應用程式開發的安全需求。

對於開發者而言,現在是時候開始規劃移轉策略,逐步淘汰對安全管理器的依賴,改採更現代的安全實踐。同時,這也是一個重新審視應用程式安全架構的好機會,確保系統能夠適應未來 Java 平台的發展方向。

本篇文章的內容為老喬原創、二創或翻譯而來。雖已善盡校對、順稿與查核義務,但人非聖賢,多少仍會有疏漏之處難以避免。如果大家有任何問題、建議或指教,都歡迎在底下留言與老喬討論!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *

10 + 4 =

目錄
返回頂端