Java安全:通过URLClassLoader加载插件的沙箱技术

34
问题概述:如何修改下面的代码,使不受信任的动态加载代码在安全沙箱中运行,而应用程序的其余部分保持不受限制?为什么URLClassLoader不能像它所说的那样处理它?
我的应用程序具有插件机制,其中第三方可以提供包含实现特定接口的类的JAR。使用URLClassLoader,我可以加载该类并实例化它,没有问题。由于代码可能不受信任,我需要防止其行为不当。例如,我在单独的线程中运行插件代码,以便如果它进入无限循环或仅需要太长时间,则可以将其终止。但是,尝试为它们设置安全沙箱,以便它们无法执行诸如建立网络连接或访问硬盘驱动器上的文件之类的操作,使我非常困扰。我的努力总是导致插件没有任何影响(它具有与应用程序相同的权限)或限制了应用程序。我希望主应用程序代码能够做几乎任何它想做的事情,但插件代码被锁定。
文档和在线资源复杂、混乱且自相矛盾。我已经在各个地方读过(例如这个问题),我需要提供自定义SecurityManager,但是当我尝试时,我遇到问题,因为JVM懒加载JAR中的类。因此,我可以很好地实例化它,但是如果我调用已从相同JAR中实例化另一个类的对象上的方法,则会失败,因为它被拒绝读取JAR的权限。
理论上,我可以在我的SecurityManager中放置FilePermission检查,以查看它是否正在尝试从其自己的JAR中加载。那很好,但是URLClassLoader文档说:“默认情况下,加载的类仅被授予访问创建URLClassLoader时指定的URL的权限。”那我为什么需要自定义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不能像它所说的那样为我处理呢?我将保留此问题一段时间,看看是否有人能回答这个问题。如果是这样,那个人将得到被接受的答案。否则,我会授予Ani B.奖励,因为URLClassLoader文档是错误的,他建议制作自定义SecurityManager是正确的。
PluginThread将不得不在PluginSecurityManager上设置classSource属性,即类文件的路径。现在,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;
    }
}

这是一个相关的问题,可能会有所帮助:https://dev59.com/f3RB5IYBdhLWcg3wz6ad :) - ramayac
是的,事实上我在我的问题中链接了那个问题。问题是,那里提出的解决方案是不完整的。它没有完全实现SecurityManager,这是这个问题的关键部分。它也没有解释为什么你需要一个SecurityManager,因为URLClassLoader声称默认情况下可以处理这个问题。 - Robert J. Walker
你知道不受信任的代码在run返回后可以在另一个线程中运行代码吗? - Tom Hawtin - tackline
我有一个非常类似的问题,正在努力理解这个解决方案。然而,在PluginThread.run方法中不是存在一个大问题吗,它会改变整个系统的安全管理器。如果使用线程运行(假设有两个人同时上传插件),这是否会导致并发问题?第二个线程启动,设置新的安全管理器,就在第一个线程完成并将其设置为“旧”的时候。 - ThePerson
3个回答

8

根据文档:
后续加载类和资源时,将使用创建URLClassLoader实例的线程的AccessControlContext。

默认情况下,加载的类仅被授予访问在创建URLClassLoader时指定的URL的权限。

URLClassLoader正如其所说的那样进行操作,需要注意的是AccessControlContext。基本上,AccessControlContext中引用的线程没有执行你期望的操作的权限。


你是对的;创建类加载器的线程的AccessControlContext允许所有操作,因此URLClassLoader也会允许。文档中的后者陈述似乎与前者相矛盾,所以我仍然觉得有些事情我没有理解,但是对于回答“为什么?”的问题,我会把答案归功于你。 - Robert J. Walker
AccessControlContext无法访问哪些资源,而URLClassLoader可以访问? - Woot4Moo

7
我在应用程序中运行一些Groovy脚本时使用以下方法。我显然希望防止脚本意外或有意地运行System.exit。
我按照通常的方式安装了Java SecurityManager:
-Djava.security.manager -Djava.security.policy=<policy file>

<策略文件>中,我赋予我的应用程序所有权限(我完全信任我的应用程序),即:
grant {
    permission java.security.AllPermission;
};

我在运行Groovy脚本的部分限制了其功能:

list = AccessController.doPrivileged(new PrivilegedExceptionAction<List<Stuff>> () {
    public List<Stuff> run() throws Exception {
        return groovyToExecute.someFunction();
    }
}, allowedPermissionsAcc);
< p > 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)});
}

现在的难点在于找到正确的权限。

如果您想允许访问某些库,则很快就会意识到它们没有考虑安全管理器并且无法非常优雅地处理它,而找出它们需要哪些权限可能会非常棘手。如果您想通过Maven Surefire插件运行UnitTests或在不同平台(如Linux / Windows)上运行,则会遇到其他问题,因为行为可能有所不同 :-(。但这些问题是另一个话题。


我知道这是对你话题的迟来回复...但最初这确实帮了我很多,直到我发现我的代码仍然可以创建新线程,那么AccessControlContext就不再适用了,你有找到解决方法吗? - skiwi
1
如果您调用 Groovy(在 Java 中测试)代码来调用 AccessController.doPrivileged(...) 来运行恶意代码,则仍将执行恶意代码。 - skiwi
你必须确保限制对这些功能的访问,那是第二个代码块。创建一个完全沙盒化的解决方案并不容易 :-( - Maze

5

实现一个SecurityManager可能是最好的选择。您需要覆盖checkPermission方法。该方法将查看传递给它的Permission对象,并确定某个操作是否危险。这样,您可以允许某些权限并禁止其他权限。

您能描述一下您使用的自定义SecurityManager吗?


你要求查看SecurityManager是正确的;我不小心把它(以及应用它的线程类)从问题中漏掉了。我已经添加了它们。 - Robert J. Walker
虽然我可以制作一个SecurityManager,禁止除了从同一个JAR中读取之外的所有操作,但URLClassLoader文档似乎表明我不必这样做。我想知道为什么URLClassLoader似乎没有按照描述的那样工作。我已经将这些细节添加到问题中。 - Robert J. Walker
我希望我能标记两个答案为正确。Woot4Moo解释了为什么它不起作用,而你提供了一个解决方法。他们都应该得到答案的认可。无论如何,我至少给了你点赞。 - Robert J. Walker

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接