JEP 411:弃用安全管理员以便移除
总结
在未来的版本中弃用安全管理员(Security Manager)。安全管理员起源于 Java 1.0。多年来,它已不再是保护客户端 Java 代码的主要手段,并且很少被用于保护服务器端代码。为了推动 Java 的发展,我们计划与旧的 Applet API (JEP 398)一起弃用安全管理员以便后续移除。
目标
- 让开发者为在未来版本的 Java 中移除安全管理器做好准备。
- 如果用户的 Java 应用依赖于安全管理器,则发出警告。
- 评估是否需要新的 API 或机制来解决安全管理器已被应用的特定狭窄使用场景,例如阻止
System::exit
。
非目标
提供一个安全管理器的替代方案并不是目标。未来的 JEP 或增强功能可能会根据需求为特定用例定义新的 API 或机制。
动机
Java 平台强调安全性。数据的完整性受到 Java 语言和虚拟机内置内存安全性的保护:变量在使用前会进行初始化,数组边界会被检查,内存释放完全自动进行。同时,数据的保密性由 Java 类库中对现代加密算法和协议(如 SHA-3、EdDSA 和 TLS 1.3)的可信实现来保护。安全性是一门动态的科学,因此我们会持续更新 Java 平台以应对新的漏洞,并反映新的行业态势,例如通过弃用弱加密协议。
安全领域中一个长期存在的元素是安全管理员(Security Manager),它起源于 Java 1.0。在浏览器下载 Java 小程序的时代,安全管理员通过在一个沙箱中运行这些小程序来保护用户机器的完整性以及数据的保密性,这个沙箱会拒绝访问文件系统或网络等资源。Java 类库的规模较小——在 Java 1.0 中只有八个 java.*
包——这使得像 java.io
中的代码在执行任何操作之前与安全管理员进行协商变得可行。安全管理员在不受信任的代码(来自远程机器的小程序)和受信任的代码(本地机器上的类)之间划出了一条明确的界限:它会批准涉及受信任代码的所有资源访问操作,但会拒绝不受信任代码的操作。
随着 Java 的关注度提升,我们引入了签名小程序(signed applets),以允许安全管理器信任远程代码,从而使小程序能够像通过命令行使用 java
运行的本地代码一样访问相同的资源。与此同时,Java 类库也在快速扩展 —— Java 1.1 引入了 JavaBeans、JDBC、反射(Reflection)、RMI 和序列化(Serialization)—— 这意味着受信任的代码可以访问重要的新资源,例如数据库连接、RMI 服务器和反射对象。允许所有受信任的代码访问所有资源是不可取的,因此在 Java 1.2 中,我们重新设计了安全管理器,专注于应用最小权限原则:默认情况下,所有代码都被视为不受信任,并受到沙盒式控制的限制,从而防止对资源的访问;用户可以通过授予特定代码库具体的权限来信任它们访问特定资源。理论上,类路径上的应用程序 JAR 文件在使用 JDK 时可能会比来自互联网的小程序受到更多限制。限制权限被视为一种约束代码中可能存在漏洞影响的方式 —— 实际上,这是一种纵深防御机制。
那么,安全管理器有抵御两种威胁的雄心:恶意意图,尤其是在远程代码中;和意外漏洞,尤其是在本地代码中。
由于 Java 平台不再支持小程序,远程代码的恶意意图威胁已经消退。Applet API 在 2017 年的 Java 9 中被弃用,然后在 2021 年的 Java 17 中被标记为移除弃用,并计划在未来版本中将其移除。运行小程序的闭源浏览器插件已于 2018 年从 Oracle 的 JDK 11 中移除,同时移除的还有闭源的 Java Web Start 技术。因此,安全管理器所防护的许多风险已不再显著。此外,安全管理器无法防护当前许多重要的风险。安全管理器无法解决行业领导者在 2020 年确定的25 个最危险问题中的 19 个问题,因此像 XML 外部实体引用(XXE)注入和不正确的输入验证等问题需要在 Java 类库中采取直接的应对措施。(例如,JAXP 可以防范 XXE 攻击和 XML 实体扩展,而序列化过滤可以在恶意数据反序列化之前防止其造成任何损害。)安全管理器还无法基于推测执行漏洞防止恶意行为。
安全管理器对恶意意图缺乏效力,这一点令人遗憾,因为安全管理器必然与 Java 类库的结构紧密交织在一起。因此,它成为一个持续的维护负担。所有新功能和 API 都必须经过评估,以确保在启用安全管理器时它们能够正确运行。基于最小权限原则的访问控制在 Java 1.0 的类库中或许是可行的,但随着 java.*
和 javax.*
包的快速增长,JDK 中出现了数十种权限和数百个权限检查点。这是一个需要保障安全的巨大表面区域,尤其是因为权限之间可能会以意想不到的方式相互作用。某些权限(例如)允许应用程序或库代码执行一系列安全操作,但其整体效果却足够不安全,以至于如果直接授予权限,则需要更强大的权限支持。
本地代码中偶然漏洞的威胁几乎无法通过安全管理器来解决。许多声称安全管理器被广泛用于保护本地代码的说法经不起推敲;它在生产环境中的使用远比许多人认为的要少。其缺乏使用的原因有很多:
-
脆弱的权限模型 — 一个希望从安全管理器获益的应用程序开发者必须仔细授予应用程序执行所有操作所需的全部权限。无法实现部分安全,即只有少数资源受到访问控制。例如,假设一个开发者担心数据被非法访问,因此希望仅授予从特定目录读取文件的权限。授予文件读取权限是不够的,因为应用程序几乎肯定会使用 Java 类库中的其他操作(例如,写入文件),而这些其他操作会被安全管理器拒绝,因为代码将不具备适当的权限。只有那些仔细记录其代码如何与 Java 类库中的安全敏感操作交互的开发者才能授予必要的权限。这不是常见的开发者工作流程。(安全管理器不允许 负权限,这种权限可以表达“授予除读取文件外的所有操作权限”。)
-
困难的编程模型 — 安全管理器通过检查导致操作的所有运行代码的权限来批准安全敏感的操作。这使得编写在安全管理器下运行的库变得困难,因为库开发者仅仅记录他们的库代码所需的权限是不够的。使用该库的应用程序开发者还必须向其应用程序代码授予相同的权限,此外还要授予已经授予该代码的任何权限。这违反了最小特权原则,因为应用程序代码可能不需要库的权限来进行自己的操作。库开发者可以通过小心使用
java.security.AccessController
API 请求安全管理器仅考虑库的权限来缓解这种权限的病毒式增长,但这种方法和其他 安全编码指南 的复杂性远远超出了大多数开发者的兴趣。对于应用程序开发者来说,阻力最小的路径通常是授予相关 JAR 文件AllPermission
,但这再次违背了最小特权原则。 -
性能不佳 — 安全管理器的核心是一个复杂的访问控制算法,通常会带来不可接受的性能损失。出于这个原因,在命令行运行的 JVM 中,默认情况下安全管理器始终处于禁用状态。这进一步降低了开发者投资使库和应用程序在安全管理器下运行的兴趣。缺乏帮助推断和验证权限的工具也是一个额外的障碍。
在安全管理员(Security Manager)推出的 25 年里,其采用率一直很低。只有极少数应用程序附带限制自身操作的策略文件(例如,ElasticSearch)。同样,只有少数框架附带策略文件(例如,Tomcat),而使用这些框架开发应用程序的开发者仍然面临一个实际上几乎无法克服的挑战:确定自己的代码以及所使用的库所需的权限。一些框架(例如,NetBeans)则避开策略文件,转而实现自定义的安全管理员,以防止插件调用 System::exit
或深入了解代码行为,比如是否打开文件和网络连接 —— 这些使用场景我们认为可以通过其他方式更好地满足。
总之,使用安全管理器开发现代 Java 应用程序并没有引起显著的兴趣。基于权限进行访问控制决策不仅笨拙、缓慢,而且在整个行业中已逐渐失宠;例如,.NET 不再支持它。更好的安全方法是在 Java 平台的较低层级提供完整性保障 —— 例如,通过加强模块边界(JEP 403)来防止访问 JDK 的实现细节,并且强化实现本身,同时通过诸如容器和虚拟机管理程序等进程外机制隔离整个 Java 运行时与敏感资源的接触。为了推动 Java 平台向前发展,我们将弃用传统的安全管理器技术,并将其从 JDK 中移除。我们计划在多个版本中逐步弃用并削弱安全管理器的功能,同时为诸如阻止 System::exit
等任务创建替代 API,以及其他被认为重要到需要替代方案的用例。
描述
在 Java 17 中,我们将:
- 弃用并计划移除大部分与安全管理器(Security Manager)相关的类和方法。
- 如果在命令行启用了安全管理器,则在启动时发出警告消息。
- 如果 Java 应用程序或库在运行时动态安装了安全管理器,则发出警告消息。
在 Java 18 中,我们将阻止 Java 应用程序或库动态安装安全管理器(Security Manager),除非最终用户明确选择允许这样做。历史上,Java 应用程序或库始终被允许动态安装安全管理器,但 自 Java 12 起,最终用户可以通过在命令行中设置系统属性 java.security.manager
为 disallow
来阻止它(java -Djava.security.manager=disallow ...
)——这会导致 System::setSecurityManager
抛出 UnsupportedOperationException
异常。从 Java 18 开始,如果未通过 java -D...
设置,java.security.manager
的默认值将变为 disallow
。因此,调用 System::setSecurityManager
的应用程序和库可能会因意外的 UnsupportedOperationException
而失败。为了使 System::setSecurityManager
像以前一样工作,最终用户需要在命令行中将 java.security.manager
设置为 allow
(java -Djava.security.manager=allow ...
)。
在 Java 18 之后的功能版本中,我们将对其他安全管理器(Security Manager)API 进行降级处理,使它们仍然存在但功能有限或完全无功能。例如,我们可能会修改 AccessController::doPrivileged
,使其仅运行给定的操作,或者修改 System::getSecurityManager
始终返回 null
。这将允许支持安全管理器并针对之前 Java 版本编译的库继续正常工作,而无需更改甚至重新编译。我们预计一旦移除这些 API 的兼容性风险降低到可接受的水平时,会将其彻底移除。
在 Java 18 之后的功能版本中,我们可能会更改 Java SE API 的定义,以便以前执行权限检查的操作在启用安全管理器的情况下不再执行这些检查,或者减少检查次数。因此,API 规范中的方法上将减少出现 @throws SecurityException
的情况。
弃用要移除的 API
安全管理器由类 java.lang.SecurityManager
和 java.lang
与 java.security
包中的一些密切相关 API 组成。我们将会通过使用 @Deprecated(forRemoval=true)
注解来最终弃用以下八个类和两个方法:
java.lang.SecurityManager
— Security Manager 的主要 API。java.lang.System::{setSecurityManager, getSecurityManager}
— 用于设置和获取 Security Manager 的方法。java.security.{Policy, PolicySpi, Policy.Parameters}
— 策略的主要 API,用于确定在 Security Manager 下运行的代码是否已被授予权限以执行特定的特权操作。java.security.{AccessController, AccessControlContext, AccessControlException, DomainCombiner}
— 访问控制器的主要 API,这是 Security Manager 默认委托权限检查的实现。如果没有 Security Manager,这些 API 将失去意义,因为某些操作在没有策略实现和虚拟机中的访问控制上下文支持时将无法正常工作。
我们还将终止弃用以下两个类和八个强烈依赖安全管理器的方法:
-
java.lang.Thread::checkAccess
、java.lang.ThreadGroup::checkAccess
和java.util.logging.LogManager::checkAccess
— 这三个方法是异常的,因为它们允许普通的 Java 代码检查是否可以被信任执行某些操作,而无需实际执行这些操作。如果没有安全管理器(Security Manager),它们没有任何作用。 -
java.util.concurrent.Executors::{privilegedCallable, privilegedCallableUsingCurrentClassLoader, privilegedThreadFactory}
— 这些工具方法只有在启用了安全管理器时才有用。 -
java.rmi.RMISecurityManager
— RMI 的安全管理器类。这个类已经过时,并在 Java 8 中被弃用。 -
javax.security.auth.SubjectDomainCombiner
和javax.security.auth.Subject::{doAsPrivileged, getSubject}
— 基于用户的授权 API,依赖于安全管理器 API,例如AccessControlContext
和DomainCombiner
。我们计划为Subject::getSubject
提供一个替代 API,因为它常用于不需要安全管理器的用例,并继续支持涉及Subject::doAs
的用例(见下文)。
由于各种原因,我们不会弃用 java.security
包中与安全管理器相关的某些类:
-
SecureClassLoader
—java.net.URLClassLoader
的超类。此外,从 Java 9 开始,SecureClassLoader
在 应用程序类加载器和平台类加载器 的实现中起着重要作用。 -
CodeSource
— 尽管CodeSource
最常用于根据代码位置授予权限,但它并不直接与安全管理器绑定,可以独立提供价值,作为一种识别代码来源的方式,并且可选地标识谁对其进行了签名。 -
ProtectionDomain
— 一些重要的 API 依赖于ProtectionDomain
,例如ClassLoader::defineClass
和Class::getProtectionDomain
。由于ProtectionDomain
包含类的CodeSource
,因此即使在没有安全管理器的情况下,它仍然具有独立的价值。 -
Permission
及其子类 — 其他重要类(如ProtectionDomain
)依赖于Permission
。然而,Permission
的许多子类特定于某些用例,这些用例在移除安全管理器后可能不再相关。这些子类的维护者可以在评估兼容性风险后分别弃用并移除它们。 -
PermissionCollection
和Permissions
— 这些类包含Permission
对象的集合,但并不直接依赖于安全管理器。 -
PrivilegedAction
、PrivilegedExceptionAction
和PrivilegedActionException
— 这些 API 不直接依赖于安全管理器,并且被javax.security.auth
API 用于身份验证和授权(参见 下文)。 -
SecurityException
— 当权限检查失败时由 Java API 抛出的运行时异常。我们可能会在稍后弃用此 API 并计划移除,但目前这样做的影响太大。
我们不会弃用 javax.security.auth.Subject::doAs
方法,因为它可以通过将 Subject
附加到线程的 AccessControlContext
上来跨 API 边界传输 Subject
,其作用类似于 ThreadLocal
。然后,底层的身份验证机制(例如,GSSAPI 的 Kerberos 实现)可以通过调用 Subject::getSubject
来获取 Subject
的凭据。这些凭据可以用于身份验证或授权目的,并且不需要启用安全管理器。然而,Subject::doAs
依赖于与安全管理器紧密相关的 API,例如 AccessControlContext
和 DomainCombiner
。因此,我们计划 创建一个不依赖于安全管理器 API 的新 API;随后我们将弃用 Subject::doAs
API 并计划将其移除。
我们不会弃用任何工具。(我们在 JDK 10 中移除了用于编辑策略文件的 policytool
图形用户界面。)
发出警告
我们将做出以下更改,以确保开发者和用户知道安全管理器已被弃用并将在未来移除。
-
如果在启动时启用了默认的安全管理器或自定义安全管理器:
java -Djava.security.manager MyApp
java -Djava.security.manager="" MyApp
java -Djava.security.manager=default MyApp
java -Djava.security.manager=com.foo.bar.Server MyApp那么在启动时会发出以下警告:
WARNING: A command line option has enabled the Security Manager
WARNING: The Security Manager is deprecated and will be removed in a future release此警告与编译时的弃用警告不同,无法被抑制。
(上面显示的四个
java -D...
调用分别将系统属性java.security.manager
设置为:空字符串、空字符串、字符串default
和自定义安全管理器的类名。这些调用是在 Java 12 之前的 Java 版本中启用安全管理器的受支持方法。Java 12 增加了对 字符串allow
和disallow
的支持,如下所示。) -
如果在启动时未启用安全管理器,但在运行时可能会动态安装:
java MyApp
java -Djava.security.manager=allow MyApp那么在启动时不会发出警告。相反,当调用
System::setSecurityManager
时,在运行时会发出警告,如下所示:WARNING: A terminally deprecated method in java.lang.System has been called
WARNING: System::setSecurityManager has been called by com.foo.bar.Server (file:/tmp/foobarserver/thing.jar)
WARNING: Please consider reporting this to the maintainers of com.foo.bar.Server
WARNING: System::setSecurityManager will be removed in a future release每个调用者只会显示一次此警告,并且与编译时的弃用警告不同,无法被抑制。
-
如果在启动时未启用安全管理器,并且系统属性
java.security.manager
被设置为disallow
:java -Djava.security.manager=disallow MyApp
那么在启动时不会发出警告,如果在运行时尝试通过调用
System::setSecurityManager
动态安装安全管理器,则也不会发出警告。然而,每次调用System::setSecurityManager
都会抛出一个带有以下详细信息的UnsupportedOperationException
:The Security Manager is deprecated and will be removed in a future release
在 Java 18 中,
disallow
将成为java.security.manager
的默认值。届时,命令行java MyApp
的效果将与在 Java 17 上使用java -Djava.security.manager=disallow MyApp
的效果相同。
未来工作
本 JEP 是关于弃用安全管理员(Security Manager),以便在未来将其移除;它并未提议现在就移除安全管理员。因此,有时间考虑安全管理员在当前有用的使用场景,以及开发其部分功能的替代方案或备选方案是否合理。以下是一些潜在的增强功能和当前正在进行的工作列表:
-
保护对本地代码的访问 — 使用安全管理器运行的应用程序可以使用权限来防止加载本地代码,从而无法通过 Java 本地接口(JNI)访问它。JNI 的计划替代方案是 外部函数和内存 API(JEP 412),它提供了一个用于与 Java 运行时之外的代码和数据进行互操作的 Java API;它将保护对本地代码的访问,而无需依赖安全管理器。
-
监控资源访问 — 使用安全管理器运行的应用程序有时会用它来监控或记录文件和网络访问等操作,但不一定限制这些操作。可能有更好的方法来监控这些类型的活动,例如使用 JDK Flight Recorder。我们将评估为 网络、文件系统和进程创建添加新的 JFR 事件,以提高应用程序的安全性,并深入了解执行这些操作的平台 API。
-
阻止
System::exit
— 一些 IDE 和框架使用自定义的安全管理器来防止应用程序调用此方法。这种用例可能会从 新 API 中受益。 -
保护反序列化 — 使用安全管理器并反序列化数据的应用程序如果未正确授予权限,则容易受到攻击(参见,例如 Java 安全编码指南 8-5)。或者,序列化过滤器(JEP 290) 允许在早期验证传入的数据,上下文特定的反序列化过滤器(JEP 415) 将提高该验证的灵活性和粒度。
-
保护 XML 处理 — 如动机中所述,JAXP 有一种模式可以更安全地处理 XML。这种模式是选择加入的,但在启用安全管理器时默认启用。我们将研究 默认启用此模式,无论是否启用了安全管理器。(XML 签名也有类似的安全部署模式,它是选择加入的,但在启用安全管理器时默认启用。从 Java 17 开始,此模式默认启用,无论是否启用了安全管理器。)
替代方案
-
保留安全管理者 API 以供希望拦截、记录和否决资源访问的自定义安全管理者扩展使用 — 移除对策略文件的支持,但保留嵌入 Java 类库中的权限检查机制。
此选项迫使开发者学习安全管理器架构的原则和最佳实践,包括权限检查的复杂科学知识,以实现资源监控这一更简单的目标。这也可能会引发关于是否值得在 JDK 中保留所有的权限检查和调用安全管理者钩子的问题 —— 在
System::exit
处是肯定的,在System::getProperty
处可能不是。我们认为,我们应该转而研究改进提供类似功能的选项,例如 JDK Flight Recorder。 -
增强安全管理者 — 增强安全管理者以应对新的使用场景或修复其众多缺点是不切实际的。默认启用安全管理者,以
AllPermission
运行代码,并记录所有权限检查,以此鼓励开发者更加重视它,这种做法并不明智。 -
保持安全管理者现状 — 继续按原样支持它,不再进一步投资改善它。
这些替代方案中的每一个都需要以接近当前的形式保留安全管理器。在维护安全管理器的几十年里,我们看到它的使用量非常少,因此我们不再愿意承担这一持续且昂贵的负担。
测试
我们将添加新的测试,以验证在命令行启用安全管理器或在运行时动态安装时是否发出警告。
风险与假设
-
安全管理器(Security Manager)自 JDK 1.0 起就一直是 Java 平台的一部分,因此其弃用和最终移除可能会对某些应用程序产生影响。不过,在此 JEP 所针对的版本中,安全管理器的全部功能仍将得到维护。应用程序在迁移到较新的 API 和机制的过程中,仍可以继续依赖受支持的 JDK 使用一段时间。
-
Jakarta EE 对安全管理器有一些要求。我们假设这些要求将会放宽或移除,以便兼容的应用程序能够在安全管理器降级并移除后,在未来的 Java 版本上运行。