使用 java securityManager 阻止我读取文件
Using java securityManager blocks me from reading files
在我的 java 代码中我调用了另一个第三方 java class.
我想抓住后者System.exit()
退出代码
所以我按照 post
中的建议使用安全管理器
问题是我现在无法读取文件,我得到权限错误
如 .
所示
如何捕捉退出代码并继续读取文件?
Published class MyClass {
class MySecurityManager extends SecurityManager {
@Override
public void checkExit(int status) {
throw new SecurityException();
}
}
public void foo() {
MySecurityManager secManager = new MySecurityManager();
System.setSecurityManager(secManager);
try {
ConfigValidator.main(new String[]{"-dirs", SdkServiceConfig.s.PROPERTIES_FILE_PATH});
new FileInputStream(new File("/Users/eladb/WorkspaceQa/sdk-service/src/main/resources/convert_conditions.sh"));
} catch (SecurityException e) {
//Do something if the external code used System.exit()
String a = "1";
}
catch (Exception e) {
logger.error("failed converting properties file to proto", e);
}
}
}
您有两个不同的问题:您受信任的代码无法读取文件,而不受信任的第三方库仍然可以不受阻碍地调用 System#exit
。前者可以通过向受信任的代码授予更多特权来轻松规避;后者有点棘手。
一些背景知识
权限分配
代码(由线程的 AccessControlContext
封装的 ProtectionDomain
)通常以两种方式分配 Permission
:静态地,通过 ClassLoader
,根据 class 定义, and/or 动态地,由 Policy
生效。其他不常遇到的替代方案也存在:例如,DomainCombiner
s 可以即时修改 AccessControlContext
s 的域(以及因此需要授权的各自代码的有效权限) ,并且自定义域实现可以使用它们自己的逻辑来进行权限暗示,可能会忽略或更改策略的语义。默认情况下,域的权限集是其静态和动态权限的联合。至于 classes 究竟如何映射到域,在很大程度上取决于加载程序的实现。默认情况下,位于相同 class 路径条目下的所有 classes,JAR'ed 或其他方式,都分组在同一域下。更严格的 class 加载程序可以选择例如为每个 class 分配一个域,这甚至可以用来防止同一包内 class 之间的通信。
权限评估
在默认 SecurityManager
下,要使特权操作(调用其体内具有 SecurityManager#checkXXX
的任何方法)成功,有效的每个域(每个方法的每个 class) AccessControlContext
必须已分配,如上所述,正在检查权限。然而,回想一下,上下文不一定代表 "the truth"(实际的调用堆栈)——系统代码在早期得到优化,而 AccessController#doPrivileged
调用以及 DomainCombiner
可能耦合到 AccessControlContext
可以修改上下文的域,因此整个授权算法。
问题和解决方法
System#exit
的问题是相应的权限 (RuntimePermission("exitVM.*")
) 是默认应用程序[=159] 静态 分配的少数权限之一=] 加载程序 (sun.misc.Launcher$AppClassLoader
) 到与从 class 路径加载的 classes 关联的所有域。
我想到了一些替代方案:
- 安装自定义
SecurityManager
拒绝特定权限,例如 class 试图终止 JVM 进程。
- 从 "remote" 位置(class 路径之外的目录)加载第三方库,使其 class 被视为 "untrusted" 代码=] 装载机.
- 正在编写和安装不同的应用程序 class 加载器,它不会分配无关的权限。
- 将自定义域组合器插入访问控制上下文,在做出授权决定时,将所有第三方域替换为没有违规权限的等效域。
为了完整起见,我应该注意,不幸的是,在 Policy
级别,无法取消静态分配的权限。
第一个选项总体上是最方便的,但我不会进一步探讨,因为:
- 默认的
SecurityManager
非常灵活,这要归功于它与之交互的少数组件(AccessController
等)。开头的背景部分旨在提醒人们这种灵活性,"quick-n'-dirty" 方法覆盖往往会削弱这种灵活性。
- 对默认实现的粗心修改可能会导致(系统)代码以奇怪的方式出现异常行为。
- 坦率地说,因为它很无聊——它是一直提倡的一刀切的解决方案,而默认管理器出于某种原因在 1.2 中被标准化的事实早已被遗忘。
第二种选择易于实施但不切实际,使开发或构建过程复杂化。假设您不打算仅以反射方式调用库,或借助 class 路径上存在的接口,它必须在开发过程中最初存在,并在执行前重新定位。
第三个是,至少在独立 Java SE 应用程序的上下文中,相当简单,不应该对性能造成太大负担。这是我在这里喜欢的方法。
最后一个选项最新颖也最不方便。它很难安全地实施,最有可能导致性能下降,并且在每次委托给不受信任的代码之前确保组合器的存在会给客户端代码带来负担。
建议的解决方案
自定义ClassLoader
以下将用作默认应用程序加载器的替换,或者作为上下文 class 加载器,或者用于加载至少不受信任的 classes 的加载器。这个实现没有什么新奇的——它所做的只是在假定所讨论的 class 不是系统加载程序时阻止委派给默认应用程序 class 加载程序。反过来,URLClassLoader#findClass
不会将 RuntimePermission("exitVM.*")
分配给它定义的 classes 的域。
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;
}
}
如果您还希望微调分配给已加载 classes 的域,您还必须覆盖 findClass
。以下加载程序的变体是一种非常粗略的尝试。 constructClassDomain
其中只为每个 class 路径条目创建一个域(这或多或少是默认设置),但可以修改以执行不同的操作。
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;
}
}
"unsafe"代码
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);
}
}
测试
包装
将加载程序和主要 class 放在一个 JAR 中(我们称之为 trusted.jar
),将不受信任的演示 class 放在另一个 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", "";
};
请注意,如果不授予扩展安全基础设施的组件(自定义 class 加载程序、策略提供程序等),实际上不可能使它们正常工作 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
,重新运行。特权操作应该会失败。
作为结束语,在调试 AccessControlException
时,运行 -Djava.security.debug=access=domain,access=failure,policy
可以帮助追踪有问题的域和策略配置问题。
在我的 java 代码中我调用了另一个第三方 java class.
我想抓住后者System.exit()
退出代码
所以我按照 post
中的建议使用安全管理器问题是我现在无法读取文件,我得到权限错误
如
如何捕捉退出代码并继续读取文件?
Published class MyClass {
class MySecurityManager extends SecurityManager {
@Override
public void checkExit(int status) {
throw new SecurityException();
}
}
public void foo() {
MySecurityManager secManager = new MySecurityManager();
System.setSecurityManager(secManager);
try {
ConfigValidator.main(new String[]{"-dirs", SdkServiceConfig.s.PROPERTIES_FILE_PATH});
new FileInputStream(new File("/Users/eladb/WorkspaceQa/sdk-service/src/main/resources/convert_conditions.sh"));
} catch (SecurityException e) {
//Do something if the external code used System.exit()
String a = "1";
}
catch (Exception e) {
logger.error("failed converting properties file to proto", e);
}
}
}
您有两个不同的问题:您受信任的代码无法读取文件,而不受信任的第三方库仍然可以不受阻碍地调用 System#exit
。前者可以通过向受信任的代码授予更多特权来轻松规避;后者有点棘手。
一些背景知识
权限分配
代码(由线程的 AccessControlContext
封装的 ProtectionDomain
)通常以两种方式分配 Permission
:静态地,通过 ClassLoader
,根据 class 定义, and/or 动态地,由 Policy
生效。其他不常遇到的替代方案也存在:例如,DomainCombiner
s 可以即时修改 AccessControlContext
s 的域(以及因此需要授权的各自代码的有效权限) ,并且自定义域实现可以使用它们自己的逻辑来进行权限暗示,可能会忽略或更改策略的语义。默认情况下,域的权限集是其静态和动态权限的联合。至于 classes 究竟如何映射到域,在很大程度上取决于加载程序的实现。默认情况下,位于相同 class 路径条目下的所有 classes,JAR'ed 或其他方式,都分组在同一域下。更严格的 class 加载程序可以选择例如为每个 class 分配一个域,这甚至可以用来防止同一包内 class 之间的通信。
权限评估
在默认 SecurityManager
下,要使特权操作(调用其体内具有 SecurityManager#checkXXX
的任何方法)成功,有效的每个域(每个方法的每个 class) AccessControlContext
必须已分配,如上所述,正在检查权限。然而,回想一下,上下文不一定代表 "the truth"(实际的调用堆栈)——系统代码在早期得到优化,而 AccessController#doPrivileged
调用以及 DomainCombiner
可能耦合到 AccessControlContext
可以修改上下文的域,因此整个授权算法。
问题和解决方法
System#exit
的问题是相应的权限 (RuntimePermission("exitVM.*")
) 是默认应用程序[=159] 静态 分配的少数权限之一=] 加载程序 (sun.misc.Launcher$AppClassLoader
) 到与从 class 路径加载的 classes 关联的所有域。
我想到了一些替代方案:
- 安装自定义
SecurityManager
拒绝特定权限,例如 class 试图终止 JVM 进程。 - 从 "remote" 位置(class 路径之外的目录)加载第三方库,使其 class 被视为 "untrusted" 代码=] 装载机.
- 正在编写和安装不同的应用程序 class 加载器,它不会分配无关的权限。
- 将自定义域组合器插入访问控制上下文,在做出授权决定时,将所有第三方域替换为没有违规权限的等效域。
为了完整起见,我应该注意,不幸的是,在 Policy
级别,无法取消静态分配的权限。
第一个选项总体上是最方便的,但我不会进一步探讨,因为:
- 默认的
SecurityManager
非常灵活,这要归功于它与之交互的少数组件(AccessController
等)。开头的背景部分旨在提醒人们这种灵活性,"quick-n'-dirty" 方法覆盖往往会削弱这种灵活性。 - 对默认实现的粗心修改可能会导致(系统)代码以奇怪的方式出现异常行为。
- 坦率地说,因为它很无聊——它是一直提倡的一刀切的解决方案,而默认管理器出于某种原因在 1.2 中被标准化的事实早已被遗忘。
第二种选择易于实施但不切实际,使开发或构建过程复杂化。假设您不打算仅以反射方式调用库,或借助 class 路径上存在的接口,它必须在开发过程中最初存在,并在执行前重新定位。
第三个是,至少在独立 Java SE 应用程序的上下文中,相当简单,不应该对性能造成太大负担。这是我在这里喜欢的方法。
最后一个选项最新颖也最不方便。它很难安全地实施,最有可能导致性能下降,并且在每次委托给不受信任的代码之前确保组合器的存在会给客户端代码带来负担。
建议的解决方案
自定义ClassLoader
以下将用作默认应用程序加载器的替换,或者作为上下文 class 加载器,或者用于加载至少不受信任的 classes 的加载器。这个实现没有什么新奇的——它所做的只是在假定所讨论的 class 不是系统加载程序时阻止委派给默认应用程序 class 加载程序。反过来,URLClassLoader#findClass
不会将 RuntimePermission("exitVM.*")
分配给它定义的 classes 的域。
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;
}
}
如果您还希望微调分配给已加载 classes 的域,您还必须覆盖 findClass
。以下加载程序的变体是一种非常粗略的尝试。 constructClassDomain
其中只为每个 class 路径条目创建一个域(这或多或少是默认设置),但可以修改以执行不同的操作。
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;
}
}
"unsafe"代码
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);
}
}
测试
包装
将加载程序和主要 class 放在一个 JAR 中(我们称之为 trusted.jar
),将不受信任的演示 class 放在另一个 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", "";
};
请注意,如果不授予扩展安全基础设施的组件(自定义 class 加载程序、策略提供程序等),实际上不可能使它们正常工作 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
,重新运行。特权操作应该会失败。
作为结束语,在调试 AccessControlException
时,运行 -Djava.security.debug=access=domain,access=failure,policy
可以帮助追踪有问题的域和策略配置问题。