JEP 486:永久禁用安全管理器
概括
多年来,安全管理器一直不是保护客户端 Java 代码的主要手段,它很少用于保护服务器端代码,而且维护成本高昂。因此,我们已弃用它,并在 Java 17 中通过JEP 411 (2021) 将其删除。作为删除安全管理器的下一步,我们将修改 Java 平台规范,以便开发人员无法启用它,并且其他平台类不会引用它。此更改不会对绝大多数应用程序、库和工具产生影响。我们将在未来的版本中删除安全管理器 API。
目标
-
删除启动 Java 运行时时启用安全管理器的功能(
java -Djava.security.manager ...
)。 -
删除在应用程序运行时安装安全管理器的功能(
System.setSecurityManager(...)
)。 -
提高当前将资源访问决策委托给安全管理器的数百个 JDK 类的可维护性。
-
修改安全管理器 API 的规范,以便其所有实现的行为都如同从未启用过安全管理器一样。
-
在此版本中保留安全管理器 API,以便依赖于它的现有代码的维护者有时间迁移出去。
非目标
- 我们的目标不是提供任何安全管理器功能的替代品,特别是沙盒化 Java 代码或拦截对 Java 平台 API 的调用的能力。
动机
安全管理器自 Java 平台首次发布以来就一直是其一项功能。它基于_最小特权原则:_默认情况下,代码不受信任,因此它无法访问文件系统或网络等资源,而开发人员通过授予特定代码访问特定资源的权限来信任它。理论上,这可以保护机器和应用程序免受包含意外漏洞或恶意编写的代码的攻击。然而,在实践中,权限方案非常复杂,以至于安全管理器始终默认处于禁用状态,并且它的使用极其罕见。
尽管默认情况下禁用安全管理器,但最小权限模型在 Java 平台库中引起了极大的复杂性。从网络、I/O 和 JDBC 到 XML、AWT 和 Swing,如果启用了安全管理器,库必须实现最小权限模型:
-
启用安全管理器后,必须有 1,000 多种方法检查访问资源的权限。例如,类的构造函数委托给安全管理器,后者会应用复杂的算法来确定是否允许访问。
FileOutputStream
-
启用安全管理器后,超过 1,200 种方法必须提升其权限。例如,如果某个应用程序没有读取文件的权限,但它调用了
java.time.LocalDateTime.now()
,则java.time
代码必须声明自己的更强的权限才能读取 JDK 的内部时区数据库文件。
OpenJDK核心库小组投入了大量的时间和精力来审查这些方法的每一个变化。每个新的 API 都必须在设计时考虑到最低权限模型,并仔细审核其实现。然而,只有极少数应用程序真正启用了安全管理器。更糟糕的是,根据我们的经验,大多数应用程序都盲目地向其代码授予所有权限,从而放弃了最低权限模型的好处。
因此,我们通过JEP 411 (2021)在 Java 17 中弃用了安全管理器并将其移除。除了彻底弃用安全管理器 API 和相关 API 之外,我们还修改了 JDK,以在启用安全管理器时发出警告消息。这些更改旨在让用户和开发人员为未来版本中安全管理器的移除做好准备。
弃用安全管理器几乎没有任何影响
随着开发人员和企业从 JDK 8 和 JDK 11 升级,JDK 17 及更高版本得到了广泛采用。我们几乎没有看到 Java 生态系统中关于这些版本在启用安全管理器时发出的警告的任何讨论。这表明安全管理器与当前的 Java 开发人员几乎完全无关。我们在JEP 411中说的似乎是正确的,
“自安全管理器推出以来的 25 年里,采用率一直很低”。
和,
“总之,人们对使用安全管理器开发现代 Java 应用程序没有太大的兴趣。”
自 JDK 17 发布以来,一些支持安全管理器的框架和工具的维护者已经取消了对它的支持;其中包括Derby、Ant、SpotBugs和Tomcat。 Jakarta EE 的维护者取消了 EE 应用程序支持安全管理器的要求。我们不知道有任何新项目支持安全管理器。
未来展望
绝大多数应用程序、库和工具不需要安全管理器,不推荐使用安全管理器,不使用安全管理器,并且如果其他代码使用安全管理器则无法工作。现在是 Java 生态系统迈出下一步并完全停止使用安全管理器的时候了。
因此,我们将修改安全管理器的规范,使开发人员无法启用它,并且我们将修改其他 Java 平台库的规范,使它们不会将资源访问决策委托给它。我们将保留该类的最小版本,java.lang.SecurityManager
以兼容仍在使用它的少数应用程序、库和工具。我们将在将来的版本中删除此类。
删除安全管理器将提高 Java 的安全性
我们相信,绝大多数 Java 开发人员都希望 OpenJDK 核心库小组专注于面向互联网的应用程序所需的实用安全功能。上述对规范的修订将使我们能够从 JDK 代码库中删除安全管理器的实现,以及数千个权限检查和特权提升。这反过来将使贡献者有更多的时间和精力用于其他工作,例如
- 实施 TLS 1.3 和HTTP/3等新协议,
- 实施现代、更强大的加密算法,如 HSS/LMS、SHA-3、RSASSA-PSS 和 EdDSA,
- 弃用和禁用弱加密协议和算法,以及
- 引入用于密钥封装和密钥派生的加密API ,为后量子加密算法提供基础支持。
大多数当代安全威胁都涉及恶意数据,而安全管理器对此缺乏防御能力。删除安全管理器的实现将使贡献者有更多的时间和精力用于直接防御恶意数据的安全功能,例如:
-
更安全的序列化——反序列化过程涉及解释可能出于恶意目的而制作的数据流。2017 年,Java 9 引入了反序列化过滤器,以便开发人员可以从一开始就防止恶意数据被反序列化。从长远来看,我们已在努力寻找更好的序列化方法。
-
更严格的 XML 处理- XML 文档可以引用 Internet 上任何地方的文档类型定义 (DTD),这会导致 JDK 打开与不受信任的计算机的网络连接。在 JDK 23 中,应用程序开发人员可以通过启动 Java 运行时
java -Djava.xml.config.file=...
并指定禁止出站连接的配置文件来锁定 XML 处理。
沙盒化 Java 代码
沙盒_是指以不同于其他代码的权限运行某些 Java 代码的能力。每段代码的权限由安全策略决定,该策略由安全管理器强制执行。以受限权限运行的代码(例如不受信任或潜在恶意代码)通常被称为处于_沙盒中。
从历史上看,安全管理器用于对小程序进行沙盒处理;我们从未建议将其用于对整个应用程序进行沙盒处理。Java 应用程序应以与本机应用程序相同的方式进行沙盒处理,使用 JDK 之外的技术(例如容器、虚拟机管理程序)和 OS 机制(例如macOS App Sandbox或Linux seccomp 功能)。与安全管理器一样,这些技术可以限制应用程序使用本地和远程资源的方式;例如,它们可以防止代码访问网络以窃取数据。然而,与安全管理器不同的是,它们被广泛采用,并且相对容易学习和有效使用。
拦截对 Java 平台 API 的调用
少数应用程序使用安全管理器并非是为了强制执行安全策略,而是将其作为拦截对 Java 平台 API 的调用的一种手段。在没有安全策略和权限检查的情况下,对 Java 平台 API 的调用不再是安全问题。真正的恶意代码有无数种方法可以绕过安全管理器对 API 调用的拦截。尽管如此,一些应用程序发现拦截很有用,尤其是阻止对诸如 之类的方法的调用System::exit
。
我们设计、制作了原型并评估了各种机制,应用程序可以使用这些机制代替安全管理器来拦截对 Java 平台 API 的调用。我们发现用例太广泛,需求太分散,无法支持引入这种机制。OpenJDK 核心库组不愿意永久维护 JDK 中任意数量的、需求定义不明确的 API 拦截点。
在大多数情况下,我们发现似乎需要拦截的问题可以在 JDK 之外得到充分解决,方法是使用源代码修改、静态代码分析和重写或在类加载时基于代理的动态代码重写等技术。请参阅附录,其中有一个代理使用动态代码重写来拦截对 的调用的示例System::exit
。
旧 Java 版本中的安全管理器
安全管理器将继续在 JDK 24 之前的每个版本中可用。由于将稳定性看得高于一切,所以应用程序部署者对采用新版本持谨慎态度,他们不太可能升级到 JDK 24,因此永远不会受到 JDK 24 或后续版本中安全管理器更改的影响。
描述
在 JDK 24 中,我们将:
- 删除启动时启用安全管理器的功能,
- 删除在运行时安装自定义安全管理器的功能,并且
- 使安全管理器 API 失去功能,以便在未来版本中删除该 API。
在 JDK 24 中启用安全管理器会出现错误
在 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 -D... 调用
java.security.manager
分别将系统属性设置为空字符串、空字符串、字符串allow
、字符串default
和自定义安全管理器的类名。) -
在运行时禁用自定义安全管理器的安装并不是错误,例如通过:
$ java -jar app.jar
$ java -Djava.security.manager=disallow -jar app.jar启动时不会发出任何警告或错误消息,并且应用程序无需安全管理器即可运行,就像以前一样。
(从 JDK 18
java.security.manager
开始disallow
的默认值为,因此java -jar app.jar
与 含义相同java -Djava.security.manager=disallow -jar app.jar
。) -
在运行时通过调用 安装安全管理器是错误的
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 中的工具来扫描已弃用的安全管理器 API(例如或 )
jdeprscan
的使用。System::setSecurityManager``java.security.Policy::setPolicy
导致安全管理器 API 无法运行
安全管理器 API 包括:
- 类中的方法
java.lang.SecurityManager
, AccessController
包中的、AccessControlContext
、Policy
和ProtectionDomain
类中的方法java.security
,以及- 类中的
getSecurityManager
和方法。setSecurityManager``java.lang.System
我们不会从 Java 24 中删除这些方法;相反,我们会降低它们的行为。它们将根据需要返回null
或false
,或传递调用者的请求,或无条件抛出SecurityException
。完整的行为更改集可在此处获得。
除了改变 API 的行为之外,我们还将:
-
删除系统策略文件,
conf/security/java.policy
。 -
导致安全管理器特有的系统属性(特别是
java.security.policy
和jdk.security.filePermCompat
)被忽略。我们稍后将记录受影响的系统属性的完整列表。 -
导致安全管理器特有的安全属性(特别是
policy.provider
、package.access
和package.definition
)被忽略。我们稍后将记录受影响的安全属性的完整列表。 -
导致系统属性的
access
和选项被忽略,因为它们不再适用。有关如何使用此系统属性的信息,请参阅《_安全开发人员指南》_中的“安全性疑难解答” 。policy``java.security.debug
Java 平台 API 中其他部分的变更
SecurityException
如果启用了安全管理器但未授予适当的权限,则平台中大约有 1,000 个构造函数和方法被指定为抛出。它们涵盖 264 个类、73 个包和 25 个模块。例如,java.base
有 640 个方法被指定为抛出SecurityException
。
在 Java 24 中,我们将修改所有此类构造函数和方法的规范,删除提及的内容,SecurityException
因为现在永远不会抛出该异常。修订后的构造函数和方法的完整列表可在此处获得。
以下是构造函数规范变更的一个例子java.io.FileOutputStream
(删除的文字为删除):
公共文件输出流(字符串名称)
抛出 FileNotFoundException
创建文件输出流,将指定的内容写入文件
名称。将创建一个新的 FileDescriptor 对象来表示此文件
联系。
首先,如果有安全管理器,其 checkWrite 方法是
以名称作为参数进行调用。
如果文件存在但是是目录而不是常规文件,
不存在但无法创建,或无法打开任何
如果是其他原因,则会抛出 FileNotFoundException。
实施要求:
使用参数名称调用此构造函数是等效的
调用新的 FileOutputStream(name, false)。
参数:
name——系统相关的文件名。
抛出:
FileNotFoundException - 如果文件存在但是是目录
不同于常规文件,不存在但无法创建,
或因其他原因无法打开
SecurityException - 如果存在安全管理器并且其 checkWrite
方法拒绝对该文件的写访问。
另请参阅:
SecurityManager.checkWrite(java.lang.String)
对支持安全管理器的库维护者的建议
少数库被设计为在启用安全管理器的情况下使用它。这些库通常采用两种习惯用法:
-
调用
System::getSecurityManager
以检查安全管理器是否已启用,如果已启用,则调用SecurityManager::checkPermission
以检查是否应授予或拒绝操作:SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(...);
} -
调用
AccessController::doPrivileged
以与调用代码具有不同权限的执行代码:SomeReturnValue v = AccessController.doPrivileged(() -> {
...
return theResult;
});
System::getSecurityManager
并且AccessController::doPrivileged
方法的行为与 JDK 17 中未启用安全管理器时的行为相同:
System::getSecurityManager
返回null
,- 方法
SecurityManager::check*
throwSecurityException
,并且 - 这六种
AccessController::doPrivileged
方法立即执行给定的操作。
因此,调用这些方法的少数库将在 JDK 24 上运行而不会发生变化。但是,我们强烈建议这些库的新版本不要调用这些方法,我们将在未来的版本中删除它们。
极少数库使用 Security Manager API 的高级部分来实现自定义执行环境。例如,库可能会调用AccessController::checkPermission
以强制执行其自己的权限模型,或调用Policy::setPolicy
以使自定义安全管理器将某些资源视为禁区。这些方法在 JDK 24 中不起作用,以便提供默认情况下不允许访问所有资源的执行环境。我们将在未来的 JDK 版本中删除它们。
未来工作
删除相关 API
我们不会从 Java 24 中的 Java 平台 API 中删除任何类或方法。在未来的版本中,我们将删除在 Java 17 中弃用的安全管理器 API。在未来的版本中,我们可能会进一步弃用和删除java.lang
和java.security
包中的其他类和方法。
-
我们现在不会弃用
SecurityException
它,因为它在 JDK 中的其他与安全管理器无关的情况下使用,尽管它的规范说,“由安全管理器抛出以指示安全违规”。以下是其(误用)示例:-
java.lang.ClassLoader::defineClass``SecurityException
如果定义的类的名称以“java.
”开头,则抛出。 -
java.lang.reflect.Constructor::setAccessible``Constructor
如果在对象上调用的构造函数则抛出它java.lang.Class
。 -
java.util.java.JarInputStream
当签名的 JAR 条目签名不正确时抛出该异常。
在重新审视这些误用之后,我们可能会
SecurityException
在未来的版本中弃用它。 -
-
在未来的版本中,我们将弃用
Permission
和相关类,例如BasicPermission
、、PermissionCollection
和Permissions
,以及包Permission
之外的子类java.security
,例如java.lang.RuntimePermission
、、java.net.NetPermission
和java.lang.reflect.ReflectPermission
。 -
在未来的版本中,我们将弃用
PrivilegedAction
、PrivilegedExceptionAction
和PrivilegedActionException
。我们在 Java 17 中没有弃用这些类,因为它们出现在与安全管理器无关的类的方法签名中。在 Java 18 中,我们添加了不使用这些类javax.security.auth.Subject
的替代方法。我们最终将删除旧方法和类。javax.security.auth.Subject``Privileged*``Privileged*
删除或修改相关功能
早期 Java 平台的各种功能都是围绕移动对象的愿景而设计的。它们使用序列化在 JVM 之间移动代码和数据,并假设应用程序将启用安全管理器来防御恶意序列化的对象。这一愿景没有获得任何支持。鉴于序列化的根本缺陷和安全管理器的使用率极低,我们要么已经删除了这些功能,要么计划这样做:
-
Java 5 中引入了 JMX 管理小程序(“m-lets”),允许在启用安全管理器的情况下动态加载和执行远程 MBean。M-lets 基本上没有用处。我们在 Java 20 中弃用了 m-let API 并将其移除,并在 Java 23 中将其移除。
-
JNDI 支持重建序列化到 LDAP 数据库 ( RFC 2713 ) 的对象。自Java 6以来,JNDI 的此功能已默认禁用,但可以通过系统属性启用。安全地使用此功能依赖于启用安全管理器,因此一旦删除安全管理器,就无法安全地使用该功能。因此,我们将在未来版本中删除此功能以及 JNDI RMI 注册表服务提供程序的远程类加载功能。
-
RMI 支持动态代码加载,但仅在启用安全管理器时才启用。RMI 的此功能自 2013 年起默认禁用。随着安全管理器的删除,不再可能使用此功能。我们可能会在未来的版本中将其删除。
另外,这些javax.xml
API 允许将 Java 源代码作为扩展函数直接嵌入到 XSLT 和 XPath 文档中。此功能默认启用,但过去在使用安全管理器运行时,此功能被禁用。我们将在未来版本中默认禁用此功能,作为更严格 XML 处理的一部分。
测试
自 JDK 1.0 以来,大约有 4,000 个测试为其开发,这反映了安全管理器 API 的广度及其在 JDK 代码库中的支持深度。它们分为三类:
-
直接执行安全管理器功能的测试,例如确保正确执行权限。
-
解决安全漏洞的测试,通常确保特定的漏洞不再可能允许不受信任的代码(例如小程序)逃离沙箱。
-
一致性测试确保安全管理器实现符合安全管理器 API 的规范。
永久禁用安全管理器将使这些测试变得无关紧要,因为该功能将不再受支持,沙盒的概念也将不复存在。包括测试在内,我们将删除超过 50,000 行代码。
替代方案
check*
安全管理器 API 中的众多方法始终会抛出异常,以避免无条件地允许以前需要权限检查的操作,因此可能不被允许。这可能会给应用程序维护人员带来不便,他们可能不得不采取一些纠正措施。另一种方法是让这些方法始终成功,但这将允许应用程序不安全地运行而不会通知维护人员。
风险和假设
-
在 JDK 24 中,尝试在命令行上启用安全管理器将立即导致错误消息,并且应用程序将无法启动。如果应用程序未启动,则下游系统可能会失败,业务流程可能会受到影响。我们假设应用程序维护人员可以通过更新其
java
命令行来避免提供该-Djava.security.manager
选项,并使用其他机制来缓解安全问题,从而应对错误。(当我们从 JDK 中删除某个功能时,我们通常会拒绝任何相关的命令行选项。这包括使用来
java -D...
设置系统属性,例如java.security.manager
。例如,在 JDK 9 中删除扩展机制时,设置java.ext.dirs
系统属性会导致错误。这会迫使应用程序维护人员迅速删除过时的选项,避免 JDK 运行时出现令人困惑或误导的选项集的情况。) -
依赖该
javax.security.auth
API 的框架可能会继续使用类中已弃用的方法Subject
,即doAs
和getSubject
。我们在 Java 17 和 18 中弃用了这些方法,因为它们的签名使用了 Security Manager API 中已弃用的类。我们在Java 18中引入了doAs
和 的替代品。由于自 Java 23 以来已经引发,我们假设框架已经意识到了弃用,并正在努力采用替代品,例如HADOOP-19212。getSubject``getSubject
UnsupportedOperationException
附录
代理是一种 Java 程序,可以在应用程序运行时更改应用程序的代码。代理通过在加载类时转换方法的字节码,或在加载类后重新定义类来实现这一点。
这是一个阻止代码调用的代理System::exit
。代理声明一个在应用程序方法premain
之前由 JVM 运行的方法。此方法注册一个_转换器_,该转换器在从类路径或模块路径加载类文件时对其进行转换。转换器将每个对 的调用重写为。main``System.exit(int)``throw new RuntimeException("System.exit not allowed")
转换器使用Class-File API读取和写入类文件中的字节码,这是 JDK 23 中的预览功能。有关详细信息,请参阅软件包java.lang.classfile
。代理的源代码使用模块导入声明导入 Class-File API 和其他 Java API ,这也是 JDK 23 中的预览功能。
import module java.base;
import module java.instrument;
public class BlockSystemExitAgent {
/*
* Before the application starts, register a transformer of class files.
*/
public static void premain(String agentArgs, Instrumentation inst) {
var transformer = new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classBytes) {
if (loader != null && loader != ClassLoader.getPlatformClassLoader()) {
return blockSystemExit(classBytes);
} else {
return null;
}
}
};
inst.addTransformer(transformer, true);
}
/*
* Rewrite every invokestatic of System::exit(int) to an athrow of RuntimeException.
*/
private static byte[] blockSystemExit(byte[] classBytes) {
var modified = new AtomicBoolean();
ClassFile cf = ClassFile.of(ClassFile.DebugElementsOption.DROP_DEBUG);
ClassModel classModel = cf.parse(classBytes);
Predicate<MethodModel> invokesSystemExit =
methodModel -> methodModel.code()
.map(codeModel ->
codeModel.elementStream()
.anyMatch(BlockSystemExitAgent::isInvocationOfSystemExit))
.orElse(false);
CodeTransform rewriteSystemExit =
(codeBuilder, codeElement) -> {
if (isInvocationOfSystemExit(codeElement)) {
var runtimeException = ClassDesc.of("java.lang.RuntimeException");
codeBuilder.new_(runtimeException)
.dup()
.ldc("System.exit not allowed")
.invokespecial(runtimeException,
"<init>",
MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V"),
false)
.athrow();
modified.set(true);
} else {
codeBuilder.with(codeElement);
}
};
ClassTransform ct = ClassTransform.transformingMethodBodies(invokesSystemExit, rewriteSystemExit);
byte[] newClassBytes = cf.transform(classModel, ct);
if (modified.get()) {
return newClassBytes;
} else {
return null;
}
}
private static boolean isInvocationOfSystemExit(CodeElement codeElement) {
return codeElement instanceof InvokeInstruction i
&& i.opcode() == Opcode.INVOKESTATIC
&& "java/lang/System".equals(i.owner().asInternalName())
&& "exit".equals(i.name().stringValue())
&& "(I)V".equals(i.type().stringValue());
}
}
您必须将代理打包在 JAR 文件中,并-javaagent
在启动应用程序时通过选项指定它:
# Compile the agent into the agentclasses directory, enabling preview features for JDK 23
$ javac --enable-preview --release 23 -d agentclasses BlockSystemExitAgent.java
# Create JAR file manifest in agent.mf
$ cat > agent.mf << EOF
Premain-Class: BlockSystemExitAgent
Can-Retransform-Classes: true
EOF
# Create the agent JAR (Note there is a period after -C agentclasses)
$ jar --create --file=BlockSystemExitAgent.jar --manifest=agent.mf -C agentclasses .
# Run application with the agent JAR, enabling preview features for JDK 23
$ java --enable-preview -javaagent:BlockSystemExitAgent.jar -jar app.jar