寻找一个安全的JVM插件沙箱API

4
当你拥有一个应用服务器,并且想要在其中运行第三方插件时,你可以使用限制性安全管理器来阻止它们做诸如 System.exit() 等操作,但这只算是半个解决方案。这些不受信任的插件仍然可能进入无限循环或者在你来不及反应之前消耗所有的自由堆内存。Thread.stop() 已被弃用,所以你不能简单地杀死一个肆意妄为的线程,而由于堆是共享的,当插件使用完所有堆内存时,不仅插件会出现 OutOfMemoryError,所有其他正在运行的线程也将出现此问题。
是否有一些开源的应用程序/API/框架可以操纵插件类的字节码,使线程可停止和/或跟踪分配情况,以便如果线程分配过多则可以将其杀死?即使代码没有准备好“封装”以“单独使用”的形式,你也可以通过插入能够随意产生异常并由另一个“管理”线程触发的代码来使 Thread 可停止,并确保该异常未被插件捕获。并且你可以添加某种计数器来计算调用和循环次数以及分配量,并让“管理”线程杀死破坏了配置限制的插件。
我认为所有这些都可以通过 ASM 来完成,但我希望它们已经被做过了。我可以让插件在它们自己的 JVM 中运行,但这将涉及大量的数据编组/解组,并且如果插件 JVM 死亡/崩溃,我仍然不知道潜在的几十个(100?)插件中哪一个是问题所在,我不可能每个插件运行一个 JVM。我已经找到了一些相关的问题,但没有解决无限循环和消耗堆内存的问题:
2个回答

1
我找到了一个非常简单的解决方案来解决“System.exec('rm -rf *')”问题:

https://svn.code.sf.net/p/loggifier/code/trunk/de.unkrig.commons.lang/src/de/unkrig/commons/lang/security/Sandbox.java

package de.unkrig.commons.lang.security;

import java.security.AccessControlContext;
import java.security.Permission;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import de.unkrig.commons.nullanalysis.Nullable;

/**
 * This class establishes a security manager that confines the permissions for code executed through specific classes,
 * which may be specified by class, class name and/or class loader.
 * <p>
 * To 'execute through a class' means that the execution stack includes the class. E.g., if a method of class {@code A}
 * invokes a method of class {@code B}, which then invokes a method of class {@code C}, and all three classes were
 * previously {@link #confine(Class, Permissions) confined}, then for all actions that are executed by class {@code C}
 * the <i>intersection</i> of the three {@link Permissions} apply.
 * <p>
 * Once the permissions for a class, class name or class loader are confined, they cannot be changed; this prevents any
 * attempts (e.g. of the confined class itself) to release the confinement.
 * <p>
 * Code example:
 * <pre>
 *  Runnable unprivileged = new Runnable() {
 *      public void run() {
 *          System.getProperty("user.dir");
 *      }
 *  };
 *
 *  // Run without confinement.
 *  unprivileged.run(); // Works fine.
 *
 *  // Set the most strict permissions.
 *  Sandbox.confine(unprivileged.getClass(), new Permissions());
 *  unprivileged.run(); // Throws a SecurityException.
 *
 *  // Attempt to change the permissions.
 *  {
 *      Permissions permissions = new Permissions();
 *      permissions.add(new AllPermission());
 *      Sandbox.confine(unprivileged.getClass(), permissions); // Throws a SecurityException.
 *  }
 *  unprivileged.run();
 * </pre>
 */
public final
class Sandbox {

    private Sandbox() {}

    private static final Map<Class<?>, AccessControlContext>
    CHECKED_CLASSES = Collections.synchronizedMap(new WeakHashMap<Class<?>, AccessControlContext>());

    private static final Map<String, AccessControlContext>
    CHECKED_CLASS_NAMES = Collections.synchronizedMap(new HashMap<String, AccessControlContext>());

    private static final Map<ClassLoader, AccessControlContext>
    CHECKED_CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<ClassLoader, AccessControlContext>());

    static {

        // Install our custom security manager.
        if (System.getSecurityManager() != null) {
            throw new ExceptionInInitializerError("There's already a security manager set");
        }
        System.setSecurityManager(new SecurityManager() {

            @Override public void
            checkPermission(@Nullable Permission perm) {
                assert perm != null;

                for (Class<?> clasS : this.getClassContext()) {

                    // Check if an ACC was set for the class.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASSES.get(clasS);
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class name.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_NAMES.get(clasS.getName());
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class loader.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_LOADERS.get(clasS.getClassLoader());
                        if (acc != null) acc.checkPermission(perm);
                    }
                }
            }
        });
    }

    // --------------------------

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * accessControlContext}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, AccessControlContext accessControlContext) {

        if (Sandbox.CHECKED_CLASSES.containsKey(clasS)) {
            throw new SecurityException("Attempt to change the access control context for '" + clasS + "'");
        }

        Sandbox.CHECKED_CLASSES.put(clasS, accessControlContext);
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * protectionDomain}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, ProtectionDomain protectionDomain) {
        Sandbox.confine(
            clasS,
            new AccessControlContext(new ProtectionDomain[] { protectionDomain })
        );
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * permissions}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, Permissions permissions) {
        Sandbox.confine(clasS, new ProtectionDomain(null, permissions));
    }

    // Code for 'CHECKED_CLASS_NAMES' and 'CHECKED_CLASS_LOADERS' omitted here.

}

0

我一直在使用OSGi框架为我的现有Web应用程序添加插件支持。根据我对这个主题的工作和阅读经验,这是我所理解的:

1)OSGi是JVM上最著名和高度支持的插件标准。有多种不同的实现,如Equinox(Eclipse),Felix(Apache),Dynamic Modules(Spring)等。因此,有很多大型开源基础工作支持。

2)规范中没有任何关于资源限制的内容。事实上,他们积极避免谈论它。并不是说他们不知道,而是他们的立场是,在JVM上,你无法阻止人们做出某些有害行为。因此,JVM上插件规范的黄金标准并不涉及此问题。

有一些信息片段(例如您发布的链接)可以了解如何实现其中一些约束条件,但是在防止恶意插件执行不良操作方面,您无能为力。

这意味着没有一种通用方法可以阻止资源占用(CPU、内存、文件描述符、SQL连接等)。

堆和CPU是比较容易处理的。但是如果只是执行一个"System.exec('rm -rf')"命令呢?或者打开大约64000个套接字,可能会导致无法创建任何新的套接字。

有太多的方式会出现问题,试图为JVM设计一个允许插件的进程内沙盒几乎是不可能的。


首先,通过使用适当配置的ClassLoader和SecurityManager,可以防止诸如System.exec()和IO之类的操作。一个插件如果无法加载某个类,那么就无法使用它,无论它多么努力。看看这个例子:它会抛出一个SecurityException!防止访问JVM外部的资源是容易的一部分,因为这种安全性已经集成在JVM中。 - Sebastien Diot
其次,我不同意你的观点,即如果你不能解决所有问题,就不应该尝试。一个小偷可以使用电锯撬开我的后门并不是让我把它留着不锁的理由。反病毒和反垃圾邮件程序也是一样。OSGi是一个极简主义标准;它只是将问题留给第三方工具来解决,而没有明确的解决方案;这并不意味着你不应该考虑它。 - Sebastien Diot

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