JVM 安全管理器文件权限 - 自定义策略
JVM Security Manager File permissions - custom policy
我在使用 JVM 安全管理器自定义策略时发现了某种意外行为。
回购:https://github.com/pedrorijo91/jvm-sec-manager
在 branch master 中,进入 /code
文件夹:
- 自定义策略文件授予文件
../allow/allow.txt
的文件读取权限
- 没有文件权限
../deny/deny.txt
- HelloWorld.java 中的代码尝试读取两个文件
- 有一个
run.sh
脚本到 运行 命令
现在一切都按预期工作:允许的文件读取,但另一个抛出安全异常:java.security.AccessControlException: access denied ("java.io.FilePermission" "../deny/deny.txt" "read")
但是如果我将两个文件(../allow/allow.txt
和 ../deny/deny.txt
)移动到 code
文件夹(更改自定义策略和 java 代码以使用这些文件) ,我也不例外。 (分支'unexpected')
当前目录是特例还是其他情况?
简要说明
这种行为记录在很多地方:
后两者重申了第一个的结束语,即:
Code can always read a file from the same directory it's in (or a subdirectory of that directory); it does not need explicit permission to do so.
也就是说,如果
(HelloWorld.class.getProtectionDomain().getCodeSource().implies(
new CodeSource(new URL("file:" + codeDir),
(Certificate[]) null)) == true)
然后 HelloWorld
将默认授予对指定目录及其后代的读取访问权限。特别是对于 code
目录 本身 这应该有一些直观的意义,否则 class 甚至无法访问 public
-access class在它的包中。
全文
基本上取决于 ClassLoader
:如果它 statically assigned any Permission
s to the ProtectionDomain
to which it mapped class——这适用于 java.net.URLClassLoader
和 sun.misc.Launcher$AppClassLoader
(OpenJDK -specific default system class loader)--这些权限将始终分配给域,无论 Policy
是否生效。
解决方法
典型的 "quick-n'-dirty" 任何与授权相关的解决方法是扩展 SecurityManager
并覆盖让你厌烦的方法;即在这种情况下,checkRead
组方法。
另一方面,对于不降低 AccessController
和朋友的灵活性的更彻底的解决方案,您必须编写一个 class 加载器,它至少会覆盖 URLClassLoader#getPermissions(CodeSource)
and/or 将加载的 classes 域的 CodeSource
限制到文件级别(URLClassLoader
和 [=33= 默认分配的域的代码源] 暗示(递归地).class 文件的 class 路径条目(JAR 或目录))。为了进一步细化,您的加载程序还可以分配您自己的域 subclass、and/or 域的实例封装您自己的 subclass 的代码源,分别覆盖 ProtectionDomain#implies(Permission)
and/or CodeSource#implies(CodeSource)
;例如,可以使前者支持 "negative permission" 语义,而后者可以将代码源暗示基于任意逻辑,可能与物理代码位置分离(想想 "trust levels")。
根据评论进行澄清
为了证明在不同的 class 加载器下这些权限实际上很重要,请考虑以下示例:有两个 classes,A
和 B
; A
有 main
方法,它只是调用 B
上的方法。此外,该应用程序是使用不同的系统 class 加载程序启动的,它 a) 在每个 class 的基础上分配域(而不是在每个 class 路径条目的基础上,因为是默认值)到 classes 它加载,没有 b) 为这些域分配任何权限。
加载程序:
package com.example.q45897574;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
public class RestrictiveClassLoader extends URLClassLoader {
private static final Pattern COMMON_SYSTEM_RESOURCE_NAMES = Pattern
.compile("(((net\.)?java)|(java(x)?)|(sun|oracle))\.[a-zA-Z0-9\.\-_\$\.]+");
private static final String OWN_CLASS_NAME = RestrictiveClassLoader.class.getName();
private static final URL[] EMPTY_URL_ARRAY = new URL[0], CLASSPATH_ENTRY_URLS;
private static final PermissionCollection NO_PERMS = new Permissions();
static {
String[] classpathEntries = AccessController.doPrivileged(new PrivilegedAction<String>() {
@Override
public String run() {
return System.getProperty("java.class.path");
}
}).split(File.pathSeparator);
Set<URL> classpathEntryUrls = new LinkedHashSet<>(classpathEntries.length, 1);
for (String classpathEntry : classpathEntries) {
try {
URL classpathEntryUrl;
if (classpathEntry.endsWith(".jar")) {
classpathEntryUrl = new URL("file:jar:".concat(classpathEntry));
}
else {
if (!classpathEntry.endsWith("/")) {
classpathEntry = classpathEntry.concat("/");
}
classpathEntryUrl = new URL("file:".concat(classpathEntry));
}
classpathEntryUrls.add(classpathEntryUrl);
}
catch (MalformedURLException mue) {
}
}
CLASSPATH_ENTRY_URLS = classpathEntryUrls.toArray(EMPTY_URL_ARRAY);
}
private static byte[] readClassData(URL classResource) throws IOException {
try (InputStream in = new BufferedInputStream(classResource.openStream());
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
while (in.available() > 0) {
out.write(in.read());
}
return out.toByteArray();
}
}
public RestrictiveClassLoader(ClassLoader parent) {
super(EMPTY_URL_ARRAY, parent);
for (URL classpathEntryUrl : CLASSPATH_ENTRY_URLS) {
addURL(classpathEntryUrl);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name == null) {
throw new ClassNotFoundException("< null >", new NullPointerException("name argument must not be null."));
}
if (OWN_CLASS_NAME.equals(name)) {
return RestrictiveClassLoader.class;
}
if (COMMON_SYSTEM_RESOURCE_NAMES.matcher(name).matches()) {
return getParent().loadClass(name);
}
Class<?> ret = findLoadedClass(name);
if (ret != null) {
return ret;
}
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String modifiedClassName = name.replace(".", "/").concat(".class");
URL classResource = findResource(modifiedClassName);
if (classResource == null) {
throw new ClassNotFoundException(name);
}
byte[] classData;
try {
classData = readClassData(classResource);
}
catch (IOException ioe) {
throw new ClassNotFoundException(name, ioe);
}
return defineClass(name, classData, 0, classData.length, constructClassDomain(classResource));
}
@Override
protected PermissionCollection getPermissions(CodeSource codesource) {
return NO_PERMS;
}
private ProtectionDomain constructClassDomain(URL codeSourceLocation) {
CodeSource cs = new CodeSource(codeSourceLocation, (Certificate[]) null);
return new ProtectionDomain(cs, getPermissions(cs), this, null);
}
}
A
:
package com.example.q45897574;
public class A {
public static void main(String... args) {
/*
* Note:
* > Can't we set the security manager via launch argument?
* No, it has to be set here, or bootstrapping will fail.
* > Why?
* Because our class loader's domain is unprivileged.
* > Can't it be privileged?
* Yes, but then everything under the same classpath entry becomes
* privileged too, because our loader's domain's code source--which
* _its own_ loader creates, thus escaping our control--implies _the
* entire_ classpath entry. There are various workarounds, which
* however fall outside of this example's scope.
*/
System.setSecurityManager(new SecurityManager());
B.b();
}
}
B
:
package com.example.q45897574;
public class B {
public static void b() {
System.out.println("success!");
}
}
非特权测试:
确保 nothing 在策略级别被授予;然后运行(假设一个基于Linux的OS——适当修改class路径):
java -cp "/home/your_user/classpath/" \
-Djava.system.class.loader=com.example.q45897574.RestrictiveClassLoader \
-Djava.security.debug=access=failure com.example.q45897574.A
你应该得到一个 NoClassDefFoundError
,以及 com.example.q45897574.A
的失败 FilePermission
。
特权测试:
现在授予 A
必要的权限(再次确保更正代码库(代码源 URL)和权限目标名称):
grant codeBase "file:/home/your_user/classpath/com/example/q45897574/A.class" {
permission java.io.FilePermission "/home/your_user/classpath/com/example/q45897574/B.class", "read";
};
...并重新运行。这次执行应该成功完成。
我在使用 JVM 安全管理器自定义策略时发现了某种意外行为。
回购:https://github.com/pedrorijo91/jvm-sec-manager
在 branch master 中,进入 /code
文件夹:
- 自定义策略文件授予文件
../allow/allow.txt
的文件读取权限
- 没有文件权限
../deny/deny.txt
- HelloWorld.java 中的代码尝试读取两个文件
- 有一个
run.sh
脚本到 运行 命令
现在一切都按预期工作:允许的文件读取,但另一个抛出安全异常:java.security.AccessControlException: access denied ("java.io.FilePermission" "../deny/deny.txt" "read")
但是如果我将两个文件(../allow/allow.txt
和 ../deny/deny.txt
)移动到 code
文件夹(更改自定义策略和 java 代码以使用这些文件) ,我也不例外。 (分支'unexpected')
当前目录是特例还是其他情况?
简要说明
这种行为记录在很多地方:
后两者重申了第一个的结束语,即:
Code can always read a file from the same directory it's in (or a subdirectory of that directory); it does not need explicit permission to do so.
也就是说,如果
(HelloWorld.class.getProtectionDomain().getCodeSource().implies(
new CodeSource(new URL("file:" + codeDir),
(Certificate[]) null)) == true)
然后 HelloWorld
将默认授予对指定目录及其后代的读取访问权限。特别是对于 code
目录 本身 这应该有一些直观的意义,否则 class 甚至无法访问 public
-access class在它的包中。
全文
基本上取决于 ClassLoader
:如果它 statically assigned any Permission
s to the ProtectionDomain
to which it mapped class——这适用于 java.net.URLClassLoader
和 sun.misc.Launcher$AppClassLoader
(OpenJDK -specific default system class loader)--这些权限将始终分配给域,无论 Policy
是否生效。
解决方法
典型的 "quick-n'-dirty" 任何与授权相关的解决方法是扩展 SecurityManager
并覆盖让你厌烦的方法;即在这种情况下,checkRead
组方法。
另一方面,对于不降低 AccessController
和朋友的灵活性的更彻底的解决方案,您必须编写一个 class 加载器,它至少会覆盖 URLClassLoader#getPermissions(CodeSource)
and/or 将加载的 classes 域的 CodeSource
限制到文件级别(URLClassLoader
和 [=33= 默认分配的域的代码源] 暗示(递归地).class 文件的 class 路径条目(JAR 或目录))。为了进一步细化,您的加载程序还可以分配您自己的域 subclass、and/or 域的实例封装您自己的 subclass 的代码源,分别覆盖 ProtectionDomain#implies(Permission)
and/or CodeSource#implies(CodeSource)
;例如,可以使前者支持 "negative permission" 语义,而后者可以将代码源暗示基于任意逻辑,可能与物理代码位置分离(想想 "trust levels")。
根据评论进行澄清
为了证明在不同的 class 加载器下这些权限实际上很重要,请考虑以下示例:有两个 classes,A
和 B
; A
有 main
方法,它只是调用 B
上的方法。此外,该应用程序是使用不同的系统 class 加载程序启动的,它 a) 在每个 class 的基础上分配域(而不是在每个 class 路径条目的基础上,因为是默认值)到 classes 它加载,没有 b) 为这些域分配任何权限。
加载程序:
package com.example.q45897574;
import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.regex.Pattern;
public class RestrictiveClassLoader extends URLClassLoader {
private static final Pattern COMMON_SYSTEM_RESOURCE_NAMES = Pattern
.compile("(((net\.)?java)|(java(x)?)|(sun|oracle))\.[a-zA-Z0-9\.\-_\$\.]+");
private static final String OWN_CLASS_NAME = RestrictiveClassLoader.class.getName();
private static final URL[] EMPTY_URL_ARRAY = new URL[0], CLASSPATH_ENTRY_URLS;
private static final PermissionCollection NO_PERMS = new Permissions();
static {
String[] classpathEntries = AccessController.doPrivileged(new PrivilegedAction<String>() {
@Override
public String run() {
return System.getProperty("java.class.path");
}
}).split(File.pathSeparator);
Set<URL> classpathEntryUrls = new LinkedHashSet<>(classpathEntries.length, 1);
for (String classpathEntry : classpathEntries) {
try {
URL classpathEntryUrl;
if (classpathEntry.endsWith(".jar")) {
classpathEntryUrl = new URL("file:jar:".concat(classpathEntry));
}
else {
if (!classpathEntry.endsWith("/")) {
classpathEntry = classpathEntry.concat("/");
}
classpathEntryUrl = new URL("file:".concat(classpathEntry));
}
classpathEntryUrls.add(classpathEntryUrl);
}
catch (MalformedURLException mue) {
}
}
CLASSPATH_ENTRY_URLS = classpathEntryUrls.toArray(EMPTY_URL_ARRAY);
}
private static byte[] readClassData(URL classResource) throws IOException {
try (InputStream in = new BufferedInputStream(classResource.openStream());
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
while (in.available() > 0) {
out.write(in.read());
}
return out.toByteArray();
}
}
public RestrictiveClassLoader(ClassLoader parent) {
super(EMPTY_URL_ARRAY, parent);
for (URL classpathEntryUrl : CLASSPATH_ENTRY_URLS) {
addURL(classpathEntryUrl);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name == null) {
throw new ClassNotFoundException("< null >", new NullPointerException("name argument must not be null."));
}
if (OWN_CLASS_NAME.equals(name)) {
return RestrictiveClassLoader.class;
}
if (COMMON_SYSTEM_RESOURCE_NAMES.matcher(name).matches()) {
return getParent().loadClass(name);
}
Class<?> ret = findLoadedClass(name);
if (ret != null) {
return ret;
}
return findClass(name);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String modifiedClassName = name.replace(".", "/").concat(".class");
URL classResource = findResource(modifiedClassName);
if (classResource == null) {
throw new ClassNotFoundException(name);
}
byte[] classData;
try {
classData = readClassData(classResource);
}
catch (IOException ioe) {
throw new ClassNotFoundException(name, ioe);
}
return defineClass(name, classData, 0, classData.length, constructClassDomain(classResource));
}
@Override
protected PermissionCollection getPermissions(CodeSource codesource) {
return NO_PERMS;
}
private ProtectionDomain constructClassDomain(URL codeSourceLocation) {
CodeSource cs = new CodeSource(codeSourceLocation, (Certificate[]) null);
return new ProtectionDomain(cs, getPermissions(cs), this, null);
}
}
A
:
package com.example.q45897574;
public class A {
public static void main(String... args) {
/*
* Note:
* > Can't we set the security manager via launch argument?
* No, it has to be set here, or bootstrapping will fail.
* > Why?
* Because our class loader's domain is unprivileged.
* > Can't it be privileged?
* Yes, but then everything under the same classpath entry becomes
* privileged too, because our loader's domain's code source--which
* _its own_ loader creates, thus escaping our control--implies _the
* entire_ classpath entry. There are various workarounds, which
* however fall outside of this example's scope.
*/
System.setSecurityManager(new SecurityManager());
B.b();
}
}
B
:
package com.example.q45897574;
public class B {
public static void b() {
System.out.println("success!");
}
}
非特权测试:
确保 nothing 在策略级别被授予;然后运行(假设一个基于Linux的OS——适当修改class路径):
java -cp "/home/your_user/classpath/" \
-Djava.system.class.loader=com.example.q45897574.RestrictiveClassLoader \
-Djava.security.debug=access=failure com.example.q45897574.A
你应该得到一个 NoClassDefFoundError
,以及 com.example.q45897574.A
的失败 FilePermission
。
特权测试:
现在授予 A
必要的权限(再次确保更正代码库(代码源 URL)和权限目标名称):
grant codeBase "file:/home/your_user/classpath/com/example/q45897574/A.class" {
permission java.io.FilePermission "/home/your_user/classpath/com/example/q45897574/B.class", "read";
};
...并重新运行。这次执行应该成功完成。