JEP 411: Deprecate the Security Manager for Removal | 弃用安全管理器以备移除
摘要
弃用安全管理器,以便在未来版本中移除。安全管理器源自 Java 1.0。多年来,它已不是保护客户端 Java 代码的主要手段,也很少用于保护服务器端代码。为了推动 Java 的发展,我们打算弃用安全管理器,以便与遗留的小程序 API(JEP 398)一起移除。
目标
- 为开发者准备在未来版本的 Java 中移除安全管理器。
- 如果 Java 应用程序依赖于安全管理器,则向用户发出警告。
- 评估是否需要新的 API 或机制来解决安全管理器所应用的特定狭窄用例,如阻止
System::exit
。
非目标
不提供安全管理器的替代品不是本项目的目标。未来的 JEP 或增强功能可能会根据需求定义新的 API 或机制,以用于特定用例。
动机
Java 平台重视安全性。数据的 完整性 受到 Java 语言和 VM 内置内存安全性的保护:变量在使用前会进行初始化,会检查数组边界,且内存释放完全自动化。同时,数据的 机密性 受到 Java 类库对现代加密算法和协议(如 SHA-3、EdDSA 和 TLS 1.3)的可信实现的保护。安全性是一门不断发展的科学,因此我们不断更新 Java 平台以应对新的漏洞并反映新的行业态势,例如弃用弱加密协议。
长期以来,安全性的一个重要元素就是安全管理器,它起源于 Java 1.0。在通过 Web 浏览器下载 Java 小程序的时代,安全管理器通过在 沙箱 中运行小程序来保护用户机器的完整性和数据的机密性,沙箱会拒绝访问文件系统或网络等资源。Java 类库规模较小——Java 1.0 中仅有八个 java.*
包——这使得例如 java.io
中的代码在执行任何操作之前咨询安全管理器变得可行。安全管理器在 不受信任的代码(来自远程机器的小程序)和 受信任的代码(本地机器上的类)之间划清了界限:它会批准受信任代码涉及资源访问的所有操作,但会拒绝不受信任代码的操作。
随着 Java 兴趣的增长,我们引入了 签名小程序,以允许安全管理器信任远程代码,从而使小程序能够访问与通过命令行上的 java
运行的本地代码相同的资源。同时,Java 类库也在迅速扩展——Java 1.1 引入了 JavaBeans、JDBC、反射、RMI 和序列化——这意味着受信任的代码可以访问重要的新资源,如数据库连接、RMI 服务器和反射对象。允许所有受信任的代码访问所有资源是不可取的,因此在 Java 1.2 中,我们重新设计了安全管理器,以专注于应用 最小权限原则:默认情况下,所有代码都将被视为不受信任,并受到沙箱式控制的约束,这些控制可防止访问资源,用户将通过授予特定代码库访问特定资源的特定权限来信任这些代码库。理论上,类路径上的应用程序 JAR 文件在使用 JDK 方面可能比来自 Internet 的小程序受到更多限制。限制权限被视为一种限制代码中可能存在的任何漏洞的影响的方法——实际上,这是一种深度防御机制。
因此,安全管理器的目标是防范两种威胁:恶意意图,特别是在远程代码中,以及 意外漏洞,特别是在本地代码中。
远程代码恶意意图的威胁已经减弱,因为 Java 平台不再支持小程序。Applet API 在 2017 年的 Java 9 中被弃用,然后在 2021 年的 Java 17 中被弃用并计划在未来的版本中移除。运行小程序的闭源浏览器插件以及 闭源 Java Web Start 技术 已在 2018 年的 Oracle JDK 11 中被移除。因此,安全管理器所防范的许多风险已不再重要。此外,安全管理器无法防范现在很重要的许多风险。安全管理器无法解决 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 默认禁用安全管理器。这进一步降低了开发人员投资使库和应用程序在安全管理器下运行的兴趣。缺乏用于帮助推断和验证权限的工具也是一个额外的障碍。
自安全管理器推出以来的四分之一世纪里,其采用率一直很低。只有少数应用程序附带用于限制其自身操作的策略文件(例如,ElasticSearch)。同样,也只有少数框架附带策略文件(例如,Tomcat),并且使用这些框架构建应用程序的开发人员仍然面临实际上难以克服的挑战,即需要弄清楚自己的代码和所使用的库所需的权限。一些框架(例如,NetBeans)则避开策略文件,而是实现自定义的安全管理器,以防止插件调用 System::exit
或深入了解代码的行为,例如它是否打开文件和网络连接——我们认为这些用例最好通过其他方式来实现。
总而言之,对于使用安全管理器来开发现代 Java 应用程序,人们并没有太大的兴趣。基于权限的访问控制决策既笨拙又缓慢,并且在整个行业中已逐渐失去青睐;例如,.NET已不再支持。通过在 Java 平台的较低级别提供完整性(例如,通过加强模块边界(JEP 403)来防止访问 JDK 实现细节,并 加固实现本身),以及通过容器和虚拟机监控程序等进程外机制将整个 Java 运行时与敏感资源隔离开来,可以更好地实现安全性。为了推动 Java 平台的发展,我们将弃用旧的安全管理器技术,并将其从 JDK 中移除。我们计划在未来的多个版本中弃用并削弱安全管理器的功能,同时创建替代 API,用于执行诸如阻止 System::exit
等任务,以及其他被认为足够重要以至于需要替代方案的使用案例。
描述
在 Java 17 中,我们将:
弃用大多数与安全管理器相关的类和方法,以便将来移除。
如果在命令行上启用了安全管理器,则在启动时发出警告消息。
如果 Java 应用程序或库动态安装了安全管理器,则在运行时发出警告消息。
在 Java 18 中,我们将阻止 Java 应用程序或库动态安装安全管理器,除非最终用户已明确选择允许。从历史上看,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 之后的特性版本中,我们将降级其他安全管理器 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
— 安全管理器的主要 API。java.lang.System::{setSecurityManager, getSecurityManager}
— 设置和获取安全管理器的方法。java.security.{Policy, PolicySpi, Policy.Parameters}
— 策略的主要 API,用于确定在安全管理器下运行的代码是否已获得执行特定特权操作的权限。java.security.{AccessController, AccessControlContext, AccessControlException, DomainCombiner}
— 访问控制器的主要 API,它是安全管理器委托权限检查的默认实现。没有安全管理器,这些 API 就没有价值,因为某些操作在没有策略实现和 VM 中的访问控制上下文支持的情况下将无法工作。
我们还将最终弃用以下两个类和八个方法,它们强烈依赖于安全管理器:
java.lang.Thread::checkAccess
、java.lang.ThreadGroup::checkAccess
和java.util.logging.LogManager::checkAccess
— 这三个方法是异常的,因为它们允许普通的 Java 代码检查它是否可信以执行某些操作,而无需实际执行它们。没有安全管理器,它们就没有用武之地。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 依赖于安全管理器 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
GUI。)
发布警告
我们将进行以下更改,以确保开发人员和用户了解安全管理器已被弃用并将被移除。
如果在启动时启用了默认安全管理器或自定义安全管理器:
bashjava -Djava.security.manager MyApp java -Djava.security.manager="" MyApp java -Djava.security.manager=default MyApp java -Djava.security.manager=com.foo.bar.Server MyApp
则在启动时发出以下警告:
txt警告:命令行选项已启用安全管理器 警告:安全管理器已被弃用,并将在未来的版本中移除
与编译时弃用警告不同,此警告无法被抑制。
(上面显示的四个
java -D...
调用分别将系统属性java.security.manager
设置为空字符串、空字符串、字符串default
以及自定义安全管理器的类名。在 Java 12 之前的版本中,这些调用是在启动时启用安全管理器的受支持方式。Java 12 增加了对 字符串allow
和disallow
的支持,如下所示。)如果启动时未启用安全管理器,但可能在运行时动态安装:
bashjava MyApp java -Djava.security.manager=allow MyApp
则在启动时不会发出警告。相反,当调用
System::setSecurityManager
时,会在运行时发出以下警告:txt警告:已调用 java.lang.System 中的最终弃用方法 警告:com.foo.bar.Server(file:/tmp/foobarserver/thing.jar)调用了 System::setSecurityManager 警告:请考虑将此情况报告给 com.foo.bar.Server 的维护者 警告:System::setSecurityManager 将在未来的版本中移除
此警告每个调用者仅显示一次,并且与编译时弃用警告不同,无法被抑制。
如果启动时未启用安全管理器,并且系统属性
java.security.manager
被设置为disallow
:bashjava -Djava.security.manager=disallow MyApp
则在启动时不会发出警告,如果在运行时尝试通过调用
System::setSecurityManager
来动态安装安全管理器,也不会在运行时发出警告。但是,每次调用System::setSecurityManager
都会抛出一个带有以下详细消息的UnsupportedOperationException
:txt安全管理器已被弃用,并将在未来的版本中移除
在 Java 18 中,
disallow
将成为java.security.manager
的默认值。因此,命令行java MyApp
将具有与 Java 17 中java -Djava.security.manager=disallow MyApp
相同的效果。
未来工作
本 JEP 是关于在未来移除安全管理器的弃用计划;它并不提议立即移除安全管理器。因此,我们有时间考虑那些当前安全管理器仍然有用的情况,以及开发其某些功能的替代品或替代方案是否合理。以下是潜在的增强功能和当前正在进行的工作列表:
保护对本地代码的访问 —— 使用安全管理器运行的应用程序可以使用权限来阻止加载本地代码,从而防止通过 Java 本地接口(JNI)进行访问。JNI 的计划替代方案是 外部函数与内存 API(JEP 412),它提供了一个 Java API,用于与 Java 运行时之外的代码和数据进行互操作;它将在不依赖安全管理器的情况下保护对本地代码的访问。
监控资源访问 —— 使用安全管理器运行的应用程序有时会使用它来监控或记录文件和网络访问等操作,而不一定限制这些操作。可能有更好的方法来监控这些类型的活动,例如使用 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
运行代码,并记录所有权限检查以鼓励开发人员更加重视它,这种做法并不明智。保留安全管理器 —— 继续按现状支持它,而不进一步投资进行改进。
这些替代方案都需要在某种程度上保留安全管理器接近其当前的形式。经过几十年的维护,但几乎看不到什么使用,我们不再愿意承担这种持续且昂贵的负担。
测试
我们将添加新的测试来验证当在命令行上启用安全管理器或在运行时动态安装时,是否会发出警告。
风险和假设
安全管理器自 JDK 1.0 以来就是 Java 平台的一部分,因此一些应用程序可能会受到其弃用和最终移除的影响。然而,在针对此 JEP 的发布版本中,将保留安全管理器的全部功能。随着应用程序迁移到更新的 API 和机制,它们可以在一段时间内继续依赖受支持的 JDK。
Jakarta EE 对安全管理器有几个要求。我们假设,为了使符合要求的应用程序在安全管理器降级并最终移除后能够在未来的 Java 版本上运行,这些要求将会放宽或移除。