Подтвердить что ты не робот

Безопасность Java: плагины для песочницы, загруженные с помощью URLClassLoader

Резюме вопроса: как мне изменить приведенный ниже код, чтобы ненадежный, динамически загруженный код работал в изолированной программной среде безопасности, в то время как остальная часть приложения остается неограниченной? Почему URLClassLoader просто обрабатывает его, как будто он говорит?

EDIT: Обновлено для ответа на Ani B.

EDIT 2: добавлен обновленный модуль PluginSecurityManager.

Мое приложение имеет механизм подключаемого модуля, в котором сторонняя организация может предоставить JAR, содержащий класс, который реализует конкретный интерфейс. Используя URLClassLoader, я могу загрузить этот класс и создать его, без проблем. Поскольку код потенциально ненадежен, мне нужно предотвратить его неправильное поведение. Например, я запускаю код подключаемого модуля в отдельном потоке, чтобы я мог его убить, если он переходит в бесконечный цикл или занимает слишком много времени. Но пытаясь установить для них изолированную программную среду безопасности, чтобы они не могли делать такие вещи, как создание сетевых подключений или доступ к файлам на жестком диске, делает меня положительно батти. Мои усилия всегда приводят к тому, что они не влияют на подключаемый модуль (он имеет те же права, что и приложение), либо также ограничивает приложение. Я хочу, чтобы основной код приложения мог делать практически все, что он хочет, но код подключаемого модуля должен быть заблокирован.

Документация и онлайн-ресурсы по этому предмету сложны, запутанны и противоречивы. Я читал в разных местах (например, этот вопрос), что мне нужно предоставить собственный SecurityManager, но когда я пытаюсь, у меня возникают проблемы, потому что JVM lazy- загружает классы в JAR. Поэтому я могу создать экземпляр, но если я вызываю метод на загружаемом объекте, который создает экземпляр другого класса из одного JAR, он взрывается, потому что он лишен права читать из JAR.

Теоретически, я могу поместить проверку на FilePermission в моем SecurityManager, чтобы проверить, пытается ли она загрузить из своего JAR. Это прекрасно, но документация URLClassLoader говорит: "Загружаемые классы по умолчанию предоставляют разрешение только для доступа к URL-адресам, указанным, когда URLClassLoader был создано". Итак, зачем мне нужен специальный SecurityManager? Должен ли URLClassLoader просто справиться с этим? Почему это не так?

Здесь приведен упрощенный пример, который воспроизводит проблему:

Основное приложение (доверенное)

PluginTest.java

package test.app;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;

import test.api.Plugin;

public class PluginTest {
    public static void pluginTest(String pathToJar) {
        try {
            File file = new File(pathToJar);
            URL url = file.toURI().toURL();
            URLClassLoader cl = new URLClassLoader(new java.net.URL[] { url });
            Class<?> clazz = cl.loadClass("test.plugin.MyPlugin");
            final Plugin plugin = (Plugin) clazz.newInstance();
            PluginThread thread = new PluginThread(new Runnable() {
                @Override
                public void run() {
                    plugin.go();
                }
            });
            thread.start();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

Plugin.java

package test.api;

public interface Plugin {
    public void go();
}

PluginSecurityManager.java

package test.app;

public class PluginSecurityManager extends SecurityManager {
    private boolean _sandboxed;

    @Override
    public void checkPermission(Permission perm) {
        check(perm);
    } 

    @Override
    public void checkPermission(Permission perm, Object context) {
        check(perm);
    }

    private void check(Permission perm) {
        if (!_sandboxed) {
            return;
        }

        // I *could* check FilePermission here, but why doesn't
        // URLClassLoader handle it like it says it does?

        throw new SecurityException("Permission denied");
    }

    void enableSandbox() {
    _sandboxed = true;
    }

    void disableSandbox() {
        _sandboxed = false;
    }
}

PluginThread.java

package test.app;

class PluginThread extends Thread {
    PluginThread(Runnable target) {
        super(target);
    }

    @Override
    public void run() {
        SecurityManager old = System.getSecurityManager();
        PluginSecurityManager psm = new PluginSecurityManager();
        System.setSecurityManager(psm);
        psm.enableSandbox();
        super.run();
        psm.disableSandbox();
        System.setSecurityManager(old);
    }
}

Плагин JAR (ненадежный)

MyPlugin.java

package test.plugin;

public MyPlugin implements Plugin {
    @Override
    public void go() {
        new AnotherClassInTheSamePlugin(); // ClassNotFoundException with a SecurityManager
        doSomethingDangerous(); // permitted without a SecurityManager
    }

    private void doSomethingDangerous() {
        // use your imagination
    }
}

UPDATE: Я изменил его так, что перед запуском кода плагина он уведомляет PluginSecurityManager, чтобы он знал, с каким классом он работает. Тогда он разрешит доступ к файлам только в файлах этого пути. Это также имеет хорошее преимущество, что я могу просто установить менеджера безопасности один раз в начале моего приложения и просто обновить его, когда я вхожу и оставляю код плагина.

Это в значительной степени решает проблему, но не отвечает на мой другой вопрос: почему URLClassLoader не обрабатывает это для меня, как будто он говорит, что это так? Я оставлю этот вопрос открытым дольше, чтобы узнать, есть ли у кого-нибудь ответ на этот вопрос. Если да, то этот человек получит принятый ответ. В противном случае я вручу его Ани Б. на предположение, что документация URLClassLoader лежит и что его совет по созданию настраиваемого SecurityManager является правильным.

ПлагинThread должен установить свойство classSource в PluginSecurityManager, который является путем к файлам классов. Теперь PluginSecurityManager выглядит примерно так:

package test.app;

public class PluginSecurityManager extends SecurityManager {
    private String _classSource;

    @Override
    public void checkPermission(Permission perm) {
        check(perm);
    } 

    @Override
    public void checkPermission(Permission perm, Object context) {
        check(perm);
    }

    private void check(Permission perm) {
        if (_classSource == null) {
            // Not running plugin code
            return;
        }

        if (perm instanceof FilePermission) {
            // Is the request inside the class source?
            String path = perm.getName();
            boolean inClassSource = path.startsWith(_classSource);

            // Is the request for read-only access?
            boolean readOnly = "read".equals(perm.getActions());

            if (inClassSource && readOnly) {
                return;
            }
        }

        throw new SecurityException("Permission denied: " + perm);
    }

    void setClassSource(String classSource) {
    _classSource = classSource;
    }
}
4b9b3361

Ответ 1

Из документов:
The AccessControlContext of the thread that created the instance of URLClassLoader will be used when subsequently loading classes and resources.

The classes that are loaded are by default granted permission only to access the URLs specified when the URLClassLoader was created.

URLClassLoader выполняет именно так, как говорится, AccessControlContext - это то, что вам нужно посмотреть. В основном поток, на который ссылается в AccessControlContext, не имеет прав, чтобы делать то, что вы думаете.

Ответ 2

Я использую следующий подход при запуске некоторого Groovy script в приложении. Я, очевидно, хочу предотвратить запуск script (намеренно или непреднамеренно) System.exit

Я устанавливаю JavaManager SecurityManager обычным способом:

-Djava.security.manager -Djava.security.policy=<policy file>

В <policy file> я предоставляю моему приложению все разрешения (полностью доверяю своему приложению), т.е.:

grant {
    permission java.security.AllPermission;
};

Я ограничиваю возможности в той части, где выполняется Groovy script:

list = AccessController.doPrivileged(new PrivilegedExceptionAction<List<Stuff>> () {
    public List<Stuff> run() throws Exception {
        return groovyToExecute.someFunction();
    }
}, allowedPermissionsAcc);

allowedPermissionsAcc не изменяется, поэтому я создаю их в статическом блоке

private static final AccessControlContext allowedPermissionsAcc; 
static {    // initialization of the allowed permissions
    PermissionCollection allowedPermissions = new Permissions();
    allowedPermissions.add(new RuntimePermission("accessDeclaredMembers"));
    // ... <many more permissions here> ...

    allowedPermissionsAcc = new AccessControlContext(new ProtectionDomain[] {
        new ProtectionDomain(null, allowedPermissions)});
}

Теперь сложная часть - найти правильные разрешения.

Если вы хотите разрешить доступ к определенным библиотекам, вы быстро поймете, что они не были написаны с учетом диспетчера безопасности и не обрабатывают один из них очень изящно, и выяснение, какие разрешения, в которых они нуждаются, могут быть довольно сложными. У вас возникнут дополнительные проблемы, если вы хотите запускать UnitTests через плагин Maven Surefire или работать на разных платформах, таких как Linux/Windows, поскольку поведение может варьироваться:-( Но эти проблемы - это еще одна тема

Ответ 3

Реализация SecurityManager, вероятно, лучший способ. Вам придется переопределить checkPermission. Этот метод будет смотреть на объект Permission, переданный ему, и определить, опасно ли какое-то действие. Таким образом, вы можете разрешить некоторые разрешения и запретить другие разрешения.

Можете ли вы описать пользовательский SecurityManager, который вы использовали?