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

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

РЕДАКТИРОВАТЬ: Обновлено, чтобы ответить на Ани Б.

РЕДАКТИРОВАТЬ 2: Добавлен обновленный PluginSecurityManager.

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

Документация и онлайн-ресурсы по этой теме являются сложными, запутанными и противоречивыми. Я читал в разных местах (например,этот вопрос) что мне нужно предоставить собственный SecurityManager, но когда я пытаюсь это сделать, у меня возникают проблемы, потому что JVM загружает классы в 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
    }
}

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

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

PluginThread должен будет установить свойство 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;
    }
}

Ответы на вопрос(3)

Ваш ответ на вопрос