JVM Security Manager File permissions - custom policy

£可爱£侵袭症+ 提交于 2019-12-06 14:27:26

Brief explanation

This behaviour is documented in a number of places:

The latter two reiterate the closing note of the first one, which states that:

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.

In other words, if

(HelloWorld.class.getProtectionDomain().getCodeSource().implies(
    new CodeSource(new URL("file:" + codeDir),
    (Certificate[]) null)) == true)

then HelloWorld will by default be granted read access to the denoted directory and its descendants. Particularly for the code directory itself this should make some intuitive sense, as otherwise the class would be unable to even access public-access classes within its very package.

The full story

It is basically up to the ClassLoader: If it statically assigned any Permissions to the ProtectionDomain to which it mapped the class--which applies to both java.net.URLClassLoader and sun.misc.Launcher$AppClassLoader (the OpenJDK-specific default system class loader)--these permissions will always be accorded to the domain, regardless of the Policy in effect.

Workarounds

The typical "quick-n'-dirty" workaround to anything authorization-related is to extend SecurityManager and override the methods irking you; i.e. in this case the checkRead group of methods.

For a more thorough solution that doesn't reduce the flexibility of AccessController and friends, on the other hand, you would have to write a class loader that at the very least overrides URLClassLoader#getPermissions(CodeSource) and/or restricts loaded classes' domains' CodeSources down to the file level (code sources of domains assigned by default by URLClassLoader and AppClassLoader imply (recursively) the .class file's classpath entry (JAR or directory)). For further granularity, your loader might as well assign instances of your own domain subclass, and/or domains encapsulating code sources of your own subclass, overriding respectively ProtectionDomain#implies(Permission) and/or CodeSource#implies(CodeSource); the former could for example be made to support "negative permission" semantics, and the latter could base code source implication on arbitrary logic, potentially decoupled from physical code location (think e.g. "trust levels").


Clarification as per the comments

To prove that under a different class loader these permissions actually matter, consider the following example: There are two classes, A and B; A has the main method, which simply calls a method on B. Additionally, the application is launched using a different system class loader, which a) assigns domains on a per-class basis (rather than on a per-classpath-entry basis, as is the default) to classes it loads, without b) assigning any permissions to these domains.

Loader:

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!");
    }
}

Unprivileged test:
Make sure nothing is granted at the policy level; then run (assuming a Linux-based OS--modify classpath as appropriate):

java -cp "/home/your_user/classpath/" \
-Djava.system.class.loader=com.example.q45897574.RestrictiveClassLoader \
-Djava.security.debug=access=failure com.example.q45897574.A

You should get a NoClassDefFoundError, along with a failed FilePermission for com.example.q45897574.A.

Privileged test:
Now grant the necessary permission to A (again make sure to correct both the codeBase (code source URL) and permission target name):

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";
};

...and re-run. This time execution should complete successfully.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!