DataWorm
DataWorm

Reputation: 73

Java Security Manager: Restrictions on code from external jar loaded via ServiceLoader

What am I trying to achieve? I am working on a java application that can be extended by additional jars that get integrated via ServiceLoader. These loaded extensions should run with some restrictions by the SecurityManager, of course simply to improve the security. As an example each Extension shall get one specific directory where it can store whatever, but access to any other file/folder should be restricted. The main application is trusted code and can therefore run without any restrictions. Furthermore the main application provides some api implementations for each extension that shall also run without restrictions. That means an extension mustn't access a file outside of its directory but when the extension is calling an api method that tries to access any other file, the access should be granted.

Question How can I achieve the mentioned behaviour that only 'direct' calls from extension classes get restricted but not any code from the main application? Running extensions in different threads/threadGroups might be a good solution anyway but since calls to the api might run under the same thread(group) it might not help to identify whether access should be restricted or not based only on the thread.

Example I created a simplified test environment. On one hand there are these two interfaces:

public interface Extension {
    void doSomethingRestricted();
    void doSameViaApi(ExtensionApi api);
}

public interface ExtensionApi {
    void doSomethingWithHigherPermissions();
}

For testing I created a jar containing this extension:

public class SomeExtension implements Extension {

    public void doSomethingRestricted() {
        System.out.println(System.getProperty("user.home"));
    }

    public void doSameViaApi(final ExtensionApi api) {
        api.doSomethingWithHigherPermissions();
    }
}

In the main application I would like do something like this:

final ExtensionApi api = () -> System.out.println(System.getProperty("user.home"));
try {
    final URLClassLoader urlClassLoader = new URLClassLoader(new URL[] { jarFile.toURI().toURL() });
    for(final Extension extension : ServiceLoader.load(Extension.class, urlClassLoader)) {
        extension.doSomethingRestricted();
        extension.doSameViaApi(api);
    }
}

So when I call extension.doSomethingRestricted(); it should result in a SecurityException but calling extension.doSameViaApi(api); should work just fine. So both methods try to do the same but one does try to do it via the api call. The only approach I could think of is iterating through the call history and checking the classloaders to analyze whether the access request is based on trusted code or extension code. But I feel like this might be a nasty error-prone solution so maybe I missed some better approaches?

Upvotes: 4

Views: 2116

Answers (1)

Uux
Uux

Reputation: 1218

First ensure your "main" JAR's classes get to enjoy full privileges. Programmatically this may be accomplished as follows:

package q46991566;

import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Policy;
import java.util.Collections;

public class Main {

    public static void main(String... args) throws Exception {
        // policy configuration contents: this JAR gets all permissions, others get nothing
        StringBuilder sb = new StringBuilder("grant {};\n\ngrant codebase \"")
                .append(Main.class.getProtectionDomain().getCodeSource().getLocation())
                .append("\" {\n\tpermission java.security.AllPermission;\n};\n");
        // temp-save the policy configuration
        Path policyPath = Files.createTempFile(null, null);
        Files.write(policyPath, Collections.singleton(sb.toString()));
        // convey to the default file-backed policy provider where to obtain its configuration from;
        // leading equals ensures only the specified config file gets processed
        System.setProperty("java.security.policy", "=".concat(policyPath.toUri().toURL().toString()));
        // establish a policy; "javaPolicy" is the default provider's standard JCA name
        Policy.setPolicy(Policy.getInstance("javaPolicy", null));
        // policy loaded; backing config no longer needed
        Files.delete(policyPath);
        // establish a security manager for enforcing the policy (the default implementation is more than
        // sufficient)
        System.setSecurityManager(new SecurityManager());

        // ...
    }

}

Alternatively, you will either have to a) modify the JRE distribution's java.policy (or specify a different configuration via the policy.url.n properties in java.security), or b) replace the implementation of the System ClassLoader with one that statically grants AllPermission to the ProtectionDomain associated with classes loaded from the "main" JAR.

Secondly, when loading Extensions from some JAR, employ a URLClassLoader subclass that a) manages extension-specific directories and b) includes a java.io.FilePermission in the permission collection being statically accorded to the protection domain mapped to its defined classes. Crude sample implementation (note that there is no persistent relationship between an extension JAR and a directory; also note that two Extensions originating from the same JAR (but loaded by different class loaders, of course) will get different directories):

package q46991566;

import java.io.FilePermission;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.cert.Certificate;
import java.util.Enumeration;
import java.util.Objects;

public final class ExtensionLoader extends URLClassLoader {

    private static void copyPermissions(PermissionCollection src, PermissionCollection dst) {
        for (Enumeration<Permission> e = src.elements(); e.hasMoreElements();) {
            dst.add(e.nextElement());
        }
    }

    private final CodeSource origin;
    private final PermissionCollection perms = new Permissions();
    private final Path baseDir;

    public ExtensionLoader(URL extensionOrigin) {
        super(new URL[] { extensionOrigin });
        origin = new CodeSource(Objects.requireNonNull(extensionOrigin), (Certificate[]) null);
        try {
            baseDir = Files.createTempDirectory(null);
            perms.add(new FilePermission(baseDir.toString().concat("/-"), "read,write,delete"));
            copyPermissions(super.getPermissions(origin), perms);
            perms.setReadOnly();
        }
        catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    @Override
    protected PermissionCollection getPermissions(CodeSource cs) {
        return (origin.implies(cs)) ? perms : super.getPermissions(cs);
    }

    // ExtensionApiImpl (or ExtensionImpl directly -- but then ExtensionLoader would have to be relocated
    // into a separate, also fully privileged JAR, accessible to the extension) can call this to relay to
    // extensions where they can persist their data
    public Path getExtensionBaseDir() {
        return baseDir;
    }

    // optionally override close() to delete baseDir early

}

Lastly, for unprivileged Extensions to be able to execute privileged operations via ExtensionApi, the latter's implementation must wrap privileged method (methods issuing SecurityManager::checkXXX requests) invocations within Privileged(Exception)Actions and pass them to AccessController::doPrivileged; e.g.:

ExtensionApi api = () -> {
    AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
        try {
            Files.write(Paths.get("/root/Documents/highly-sensitive.doc"), Collections.singleton("trusted content"),
                    StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.APPEND);
            return null;
        }
        catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    });
};

For details on the (proper) use of "privileged blocks", refer to the AccessController documentation and the "Secure Coding Guidelines for Java SE" document.

Upvotes: 3

Related Questions