Резюме вопроса: как мне изменить приведенный ниже код, чтобы ненадежный, динамически загруженный код работал в изолированной программной среде безопасности, в то время как остальная часть приложения остается неограниченной? Почему 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;
}
}