Java Security Manager: ограничения на код из внешнего jar, загруженного через ServiceLoader

Чего я пытаюсь достичь? Я работаю над Java-приложением, которое может быть расширено дополнительными банками, которые интегрируются через ServiceLoader. Эти загруженные расширения должны запускаться с некоторыми ограничениями SecurityManager, разумеется, просто для повышения безопасности. В качестве примера каждое Расширение должно получить один конкретный каталог, в котором оно может хранить что угодно, но доступ к любому другому файлу / папке должен быть ограничен. Основным приложением является доверенный код, и поэтому он может работать без каких-либо ограничений. Кроме того, основное приложение предоставляет некоторые реализации API для каждого расширения, которые также должны работать без ограничений. Это означает, что расширение не должно обращаться к файлу за пределами своего каталога, но когда расширение вызывает метод API, который пытается получить доступ к любому другому файлу, доступ должен быть предоставлен.

Вопрос Как я могу добиться упомянутого поведения, когда ограничиваются только "прямые" вызовы из классов расширений, но нет кода из основного приложения? Запуск расширений в разных потоках / threadGroups может быть хорошим решением в любом случае, но, поскольку вызовы API могут выполняться в одном потоке (группе), это может не помочь определить, должен ли доступ быть ограничен или не основан только на потоке.

Пример Я создал упрощенную тестовую среду. С одной стороны, есть эти два интерфейса:

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

public interface ExtensionApi {
    void doSomethingWithHigherPermissions();
}

Для тестирования я создал банку, содержащую это расширение:

public class SomeExtension implements Extension {

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

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

В основном приложении я хотел бы сделать что-то вроде этого:

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

Поэтому, когда я звоню extension.doSomethingRestricted(); это должно привести к SecurityException, но вызов extension.doSameViaApi(api); должно работать просто отлично. Таким образом, оба метода пытаются сделать то же самое, но один пытается сделать это через вызов API. Единственный подход, о котором я мог подумать, - это перебирать историю вызовов и проверять загрузчики классов, чтобы проанализировать, основан ли запрос на доступе на доверенном коде или коде расширения. Но я чувствую, что это может быть неприятное, подверженное ошибкам решение, так что, может быть, я пропустил несколько лучших подходов?

1 ответ

Во-первых, убедитесь, что ваши "основные" классы JAR пользуются всеми привилегиями. Программно это может быть выполнено следующим образом:

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());

        // ...
    }

}

В качестве альтернативы, вам придется либо) изменить дистрибутив JRE java.policy (или укажите другую конфигурацию через policy.url.n свойства в java.security) или б) заменить внедрение Системы ClassLoader с тем, который статически дает AllPermission к ProtectionDomain связан с классами, загруженными из "основного" JAR.

Во-вторых, при загрузке Extensionс какой-то JAR, нанять URLClassLoader подкласс, который а) управляет специфичными для расширения каталогами и б) включает в себя java.io.FilePermission в коллекции разрешений, статически предоставляемой домену защиты, сопоставленному с его определенными классами. Пример грубой реализации (обратите внимание, что между расширением JAR и каталогом нет постоянной связи; также обратите внимание, что два Extensions, исходящие из одного JAR (но загруженного разными загрузчиками классов, конечно) получат разные каталоги):

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

}

Наконец, для непривилегированных Extensionчтобы иметь возможность выполнять привилегированные операции через ExtensionApi, реализация последнего должна обернуть привилегированный метод (методы выдачи SecurityManager::checkXXX запросы) вызовы в Privileged(Exception)Actionи передать их AccessController::doPrivileged; например:

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

Подробнее о (правильном) использовании "привилегированных блоков" см. AccessController документация и документ"Руководство по безопасному кодированию для Java SE".

Другие вопросы по тегам