您有两个不同的问题:您的受信任代码无法读取文件,而不受信任的第三方库仍然可以不受阻碍地调用System#exit。前者可以通过授予可信代码进一步的权限来轻松规避;后者有点难以解决。
一点背景
权限分配
代码(由线程的AccessControlContext 封装的ProtectionDomains)通常以两种方式分配Permissions:静态地,由ClassLoader,在类定义时,和/或动态地,由Policy 生效.也存在其他不太常见的替代方案:例如,DomainCombiners 可以动态修改 AccessControlContexts 的域(以及因此需要授权的各自代码的有效权限),以及自定义域实现可以使用他们自己的逻辑来暗示权限,可能会忽略或改变策略的语义。默认情况下,域的权限集是其静态和动态权限的联合。至于类如何准确地映射到域,在很大程度上取决于加载器的实现。默认情况下,位于同一类路径条目下的所有类,无论是 JAR 类还是其他类,都分组在同一域下。更严格的类加载器可以选择例如为每个类分配一个域,这可用于阻止同一包内的类之间的通信。
权限评估
在默认的SecurityManager 下,为了使特权操作(调用在其主体中具有SecurityManager#checkXXX 的任何方法)成功,必须分配有效AccessControlContext 的每个域(每个方法的每个类),如上所述,正在检查权限。然而请回想一下,上下文不一定代表“真相”(实际的调用堆栈)——系统代码在早期就被优化掉了,而AccessController#doPrivileged 调用以及可能与AccessControlContext 耦合的DomainCombiner 可以修改上下文的域,以及整个授权算法。
问题和解决方法
System#exit 的问题在于,相应的权限 (RuntimePermission("exitVM.*")) 是默认应用程序类加载器 (sun.misc.Launcher$AppClassLoader) 为所有与从类路径加载的类。
我想到了许多替代方案:
- 安装一个自定义的
SecurityManager,它基于例如试图终止 JVM 进程的类拒绝特定的权利。
- 从“远程”位置(类路径之外的目录)加载第三方库,以便其类加载器将其视为“不受信任”代码。
- 编写和安装不同的应用程序类加载器,它不会分配无关的权限。
- 将自定义域组合器插入访问控制上下文中,这会在授权决策时将所有第三方域替换为没有违规权限的等效域。
为了完整起见,我应该注意,不幸的是,在Policy 级别,无法取消静态分配的权限。
第一个选项总体上是最方便的,但我不会进一步探讨它,因为:
- 默认的
SecurityManager 非常灵活,这要归功于它与之交互的少数组件(AccessController 等)。开头的背景部分旨在提醒人们注意这种灵活性,“quick-n'-dirty”方法覆盖往往会削弱这种灵活性。
- 对默认实现的粗心修改可能会导致(系统)代码行为异常。
- 坦率地说,因为它很无聊——它是永远提倡的一刀切解决方案,而默认管理器出于某种原因在 1.2 中被标准化这一事实早已被遗忘。
第二种选择很容易实现但不切实际,会使开发或构建过程复杂化。假设您不打算仅以反射方式调用库,或借助类路径中存在的接口,它必须在开发期间最初存在,并在执行之前重新定位。
第三个是,至少在独立的 Java SE 应用程序的上下文中,相当简单,不应该对性能造成太大的负担。这是我在此偏爱的方法。
最后一个选项是最新颖、最不方便的。它很难安全地实现,性能下降的可能性最大,并且在每次委托给不受信任的代码之前确保组合器的存在给客户端代码带来负担。
建议的解决方案
自定义ClassLoader
以下将用作默认应用程序加载器的替换,或者作为上下文类加载器,或者用于加载至少不受信任的类的加载器。这个实现没有什么新奇之处——它所做的只是在假设所讨论的类不是系统类时阻止委托给默认的应用程序类加载器。反过来,URLClassLoader#findClass 不会将 RuntimePermission("exitVM.*") 分配给它定义的类的域。
package com.example.trusted;
import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.regex.Pattern;
public class ClasspathClassLoader extends URLClassLoader {
private static final Pattern SYSTEM_CLASS_PREFIX = Pattern.compile("((java(x)?|sun|oracle)\\.).*");
public ClasspathClassLoader(ClassLoader parent) {
super(new URL[0], parent);
String[] classpath = System.getProperty("java.class.path").split(File.pathSeparator);
for (String classpathEntry : classpath) {
try {
if (!classpathEntry.endsWith(".jar") && !classpathEntry.endsWith("/")) {
// URLClassLoader assumes paths without a trailing '/' to be JARs by default
classpathEntry += "/";
}
addURL(new URL("file:" + classpathEntry));
}
catch (MalformedURLException mue) {
System.err.println(MessageFormat.format("Erroneous class path entry [{0}] skipped.", classpathEntry));
}
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> ret;
synchronized (getClassLoadingLock(name)) {
ret = findLoadedClass(name);
if (ret != null) {
return ret;
}
if (SYSTEM_CLASS_PREFIX.matcher(name).matches()) {
return super.loadClass(name, resolve);
}
ret = findClass(name);
if (resolve) {
resolveClass(ret);
}
}
return ret;
}
}
如果您还希望微调分配给已加载类的域,则还必须覆盖 findClass。加载程序的以下变体是这样做的非常粗略的尝试。 constructClassDomain 在其中只为每个类路径条目创建一个域(这或多或少是默认值),但可以修改以做不同的事情。
package com.example.trusted;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
public final class ClasspathClassLoader extends URLClassLoader {
private static final Pattern SYSTEM_CLASS_PREFIX = Pattern.compile("((java(x)?|sun|oracle)\\.).*");
private static final List<WeakReference<ProtectionDomain>> DOMAIN_CACHE = new ArrayList<>();
// constructor, loadClass same as above
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
URL classOrigin = getClassResource(name);
if (classOrigin == null) {
return super.findClass(name);
}
URL classCodeSourceOrigin = getClassCodeSourceResource(classOrigin);
if (classCodeSourceOrigin == null) {
return super.findClass(name);
}
return defineClass(name, readClassData(classOrigin), constructClassDomain(classCodeSourceOrigin));
}
private URL getClassResource(String name) {
return AccessController.doPrivileged((PrivilegedAction<URL>) () -> getResource(name.replace(".", "/") + ".class"));
}
private URL getClassCodeSourceResource(URL classResource) {
for (URL classpathEntry : getURLs()) {
if (classResource.getPath().startsWith(classpathEntry.getPath())) {
return classpathEntry;
}
}
return null;
}
private ByteBuffer readClassData(URL classResource) {
try {
BufferedInputStream in = new BufferedInputStream(classResource.openStream());
ByteArrayOutputStream out = new ByteArrayOutputStream();
int i;
while ((i = in.read()) != -1) {
out.write(i);
}
return ByteBuffer.wrap(out.toByteArray());
}
catch (IOException ioe) {
throw new RuntimeException(ioe);
}
}
private ProtectionDomain constructClassDomain(URL classCodeSourceResource) {
ProtectionDomain ret = getCachedDomain(classCodeSourceResource);
if (ret == null) {
CodeSource cs = new CodeSource(classCodeSourceResource, (Certificate[]) null);
DOMAIN_CACHE.add(new WeakReference<>(ret = new ProtectionDomain(cs, getPermissions(cs), this, null)));
}
return ret;
}
private ProtectionDomain getCachedDomain(URL classCodeSourceResource) {
for (WeakReference<ProtectionDomain> domainRef : DOMAIN_CACHE) {
ProtectionDomain domain = domainRef.get();
if (domain == null) {
DOMAIN_CACHE.remove(domainRef);
}
else if (domain.getCodeSource().implies(new CodeSource(classCodeSourceResource, (Certificate[]) null))) {
return domain;
}
}
return null;
}
}
“不安全”代码
package com.example.untrusted;
public class Test {
public static void testExitVm() {
System.out.println("May I...?!");
System.exit(-1);
}
}
入口点
package com.example.trusted;
import java.security.AccessControlException;
import java.security.Permission;
import com.example.untrusted.Test;
public class Main {
private static final Permission EXIT_VM_PERM = new RuntimePermission("exitVM.*");
public static void main(String... args) {
System.setSecurityManager(new SecurityManager());
try {
Test.testExitVm();
}
catch (AccessControlException ace) {
Permission deniedPerm = ace.getPermission();
if (EXIT_VM_PERM.implies(deniedPerm)) {
ace.printStackTrace();
handleUnauthorizedVmExitAttempt(Integer.parseInt(deniedPerm.getName().replace("exitVM.", "")));
}
}
}
private static void handleUnauthorizedVmExitAttempt(int exitCode) {
System.out.println("here let me do it for you");
System.exit(exitCode);
}
}
测试
包装
将加载程序和主类放在一个 JAR 中(我们称之为 trusted.jar),将演示不受信任的类放在另一个 JAR 中(untrusted.jar)。
分配权限
默认的Policy (sun.security.provider.PolicyFile) 由<JRE>/lib/security/java.policy 的文件以及<JRE>/lib/security/java.security 中的policy.url.n 属性引用的任何文件提供支持。修改前者(后者希望默认为空)如下:
// Standard extensions get all permissions by default
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
// no default permissions
grant {};
// trusted code
grant codeBase "file:///path/to/trusted.jar" {
permission java.security.AllPermission;
};
// third-party code
grant codeBase "file:///path/to/untrusted.jar" {
permission java.lang.RuntimePermission "exitVM.-1", "";
};
请注意,如果不授予它们AllPermission,几乎不可能让扩展安全基础架构的组件(自定义类加载器、策略提供程序等)正常工作。
跑步
运行:
java -classpath "/path/to/trusted.jar:/path/to/untrusted.jar" -Djava.system.class.loader=com.example.trusted.ClasspathClassLoader com.example.trusted.Main
特权操作应该会成功。
接下来在策略文件中注释掉untrusted.jar 下的RuntimePermission,然后重新运行。特权操作应该失败。
最后,在调试 AccessControlExceptions 时,使用 -Djava.security.debug=access=domain,access=failure,policy 运行可以帮助追踪违规域和策略配置问题。