在Java应用程序中防范恶意代码的沙箱机制

97
在一个模拟服务器环境中,允许用户提交他们自己的代码以便由服务器运行。显然,让任何用户提交的代码在沙盒中运行是有优势的,就像Applets在浏览器中一样。我希望能够利用JVM本身,而不是添加另一个VM层来隔离这些提交的组件。
现有的Java沙箱模型似乎可以实现这种限制,但是否有动态方式仅针对运行应用程序的用户提交部分启用它呢?
7个回答

116
  1. 将不受信任的代码在其自己的线程中运行。例如,这可以防止无限循环等问题,并使后续步骤更容易。让主线程等待该线程完成,如果时间太长,则使用Thread.stop将其终止。Thread.stop已被弃用,但由于不受信任的代码不应访问任何资源,因此安全地将其终止是可以的。

  2. 在该线程上设置SecurityManager。创建一个SecurityManager子类,覆盖checkPermission(Permission perm)以简单地抛出SecurityException以获取除少数权限之外的所有权限。这里有一些方法及其所需权限的列表:JavaTM 6 SDK中的权限

  3. 使用自定义ClassLoader加载不受信任的代码。您的类加载器将为不受信任的代码使用的所有类调用,因此您可以执行诸如禁用对个别JDK类的访问之类的操作。要做的事情是拥有允许的JDK类的白名单。

  4. 您可能希望在单独的JVM中运行不受信任的代码。虽然前面的步骤会使代码变得安全,但孤立的代码仍然可以做一件烦人的事情:分配尽可能多的内存,从而导致主应用程序的可见占用空间增加。

JSR 121: 应用程序隔离 API 规范的设计是为了解决这个问题,但不幸的是它还没有实现。

这是一个相当详细的话题,我大部分都是靠脑子想出来的。

但无论如何,以下是一些不完美、自行决定使用风险高且可能有 bug 的(伪)代码:

ClassLoader

class MyClassLoader extends ClassLoader {
  @Override
  public Class<?> loadClass(String name) throws ClassNotFoundException {
    if (name is white-listed JDK class) return super.loadClass(name);
    return findClass(name);
  }
  @Override
  public Class findClass(String name) {
    byte[] b = loadClassData(name);
    return defineClass(name, b, 0, b.length);
  }
  private byte[] loadClassData(String name) {
    // load the untrusted class data here
  }
}

安全管理器

class MySecurityManager extends SecurityManager {
  private Object secret;
  public MySecurityManager(Object pass) { secret = pass; }
  private void disable(Object pass) {
    if (pass == secret) secret = null;
  }
  // ... override checkXXX method(s) here.
  // Always allow them to succeed when secret==null
}

线程

class MyIsolatedThread extends Thread {
  private Object pass = new Object();
  private MyClassLoader loader = new MyClassLoader();
  private MySecurityManager sm = new MySecurityManager(pass);
  public void run() {
    SecurityManager old = System.getSecurityManager();
    System.setSecurityManager(sm);
    runUntrustedCode();
    sm.disable(pass);
    System.setSecurityManager(old);
  }
  private void runUntrustedCode() {
    try {
      // run the custom class's main method for example:
      loader.loadClass("customclassname")
        .getMethod("main", String[].class)
        .invoke(null, new Object[]{...});
    } catch (Throwable t) {}
  }
}

5
那段代码可能需要一些修改。无法完全保证JVM的可用性。需要准备好杀死进程(最好是自动化)。代码有可能运行在其他线程上,比如说finalizer线程。在Java库代码中使用Thread.stop会带来问题。同样地,Java库代码需要权限。最好还是允许SecurityManager使用java.security.AccessController。类加载器也应该允许访问用户代码的自身类。 - Tom Hawtin - tackline
4
鉴于这是如此复杂的主题,是否已经有处理Java“插件”的安全解决方案? - Nick Spacek
12
该方法的问题在于,当您将SecurityManager设置为System时,它不仅会影响正在运行的线程,还会影响其他线程! - Gelin Luo
3
抱歉,但 thread.stop() 可以被 throwable 捕捉到。你可以使用 while (thread.isAlive) Thread.stop(),但此时我可以递归调用一个捕获该异常的函数。在我的电脑上测试过,递归函数比 stop() 更具优势。现在你有一个垃圾线程,会占用 CPU 和资源。 - Lesto
11
除了System.setSecurityManager(…)会对整个JVM产生影响而不仅仅是调用该方法的线程之外,基于线程进行安全决策的想法在Java从1.0转换到1.1时已被放弃。当时认识到,无论哪个线程执行代码,不受信任的代码可能会调用受信任的代码,反之亦然。没有开发人员应该重复这个错误。 - Holger
显示剩余5条评论

18

显然,这种方案引起了各种安全问题。Java有一个严格的安全框架,但它并不简单。不能忽视搞砸它并让非特权用户访问重要系统组件的可能性。

尽管如此,在以源代码形式接收用户输入时,你需要做的第一件事是将其编译为Java字节码。我所知道的是,无法通过本地方式完成这个任务,因此你需要调用javac进行系统调用,并将源代码编译为存储在磁盘上的字节码。这里有一个可以作为起点的教程。 编辑:根据评论中的信息,实际上可以使用javax.tools.JavaCompiler从源代码本地编译Java代码。

一旦您有JVM字节码,就可以使用ClassLoader的 defineClass函数将其加载到JVM中。要为此加载的类设置安全上下文,您需要指定ProtectionDomainProtectionDomain的最小构造函数需要CodeSource和PermissionCollection。PermissionCollection是这里主要使用的对象-您可以使用它来指定已加载类具有的确切权限。这些权限应由JVM的AccessController最终执行。
这里有很多可能出错的地方,您应该非常小心地完全理解所有内容后再实施任何操作。

2
使用JDK 6的javax.tools API编译Java非常容易。 - Alan Krueger

12

Java-Sandbox是一个用于执行Java代码的库,它具有一组有限的权限。

可以使用它来允许访问仅限于一组白名单类和资源。它似乎不能限制对个别方法的访问。它使用自定义类加载器和安全管理器来实现这一点。

我没有使用过它,但它看起来设计良好且文档合理。

@waqas给出了一个非常有趣的答案,解释了如何自己实现这个库。但是,将这样的安全关键和复杂的代码留给专家会更加安全。

注意: 该项目自2013年以来未更新,创建者将其描述为“试验性的”。它的主页已经消失了,但Source Forge条目仍然存在。

从项目网站改编的示例代码:

SandboxService sandboxService = SandboxServiceImpl.getInstance();

// Configure context 
SandboxContext context = new SandboxContext();
context.addClassForApplicationLoader(getClass().getName());
context.addClassPermission(AccessType.PERMIT, "java.lang.System");

// Whithout this line we get a SandboxException when touching System.out
context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream");

String someValue = "Input value";

class TestEnvironment implements SandboxedEnvironment<String> {
    @Override
    public String execute() throws Exception {
        // This is untrusted code
        System.out.println(someValue);
        return "Output value";
    }
};

// Run code in sandbox. Pass arguments to generated constructor in TestEnvironment.
SandboxedCallResult<String> result = sandboxService.runSandboxed(TestEnvironment.class, 
    context, this, someValue);

System.out.println(result.get());

6
以下是关于该问题的线程安全解决方案:

点击此处

。这里提供了一个可行的解决方案。
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.

}

请留言!

CU

阿诺


为了避免循环调用(最终导致堆栈溢出),请检查安全管理器本身是否在调用堆栈中出现两次。您可以将以下代码作为for-each循环的第一行添加:if (clasS == this.getClass() && ++visitedThis >= 2) return;。(并在循环之前声明一个变量int visitedThis = 0。) - undefined

6
为了解决接受答案中的问题,即自定义SecurityManager将应用于JVM中的所有线程而不是每个线程,您可以创建一个自定义SecurityManager,并按如下方式为特定线程启用/禁用它:
import java.security.Permission;

public class SelectiveSecurityManager extends SecurityManager {

  private static final ToggleSecurityManagerPermission TOGGLE_PERMISSION = new ToggleSecurityManagerPermission();

  ThreadLocal<Boolean> enabledFlag = null;

  public SelectiveSecurityManager(final boolean enabledByDefault) {

    enabledFlag = new ThreadLocal<Boolean>() {

      @Override
      protected Boolean initialValue() {
        return enabledByDefault;
      }

      @Override
      public void set(Boolean value) {
        SecurityManager securityManager = System.getSecurityManager();
        if (securityManager != null) {
          securityManager.checkPermission(TOGGLE_PERMISSION);
        }
        super.set(value);
      }
    };
  }

  @Override
  public void checkPermission(Permission permission) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission);
    }
  }

  @Override
  public void checkPermission(Permission permission, Object context) {
    if (shouldCheck(permission)) {
      super.checkPermission(permission, context);
    }
  }

  private boolean shouldCheck(Permission permission) {
    return isEnabled() || permission instanceof ToggleSecurityManagerPermission;
  }

  public void enable() {
    enabledFlag.set(true);
  }

  public void disable() {
    enabledFlag.set(false);
  }

  public boolean isEnabled() {
    return enabledFlag.get();
  }

}

ToggleSecurirtyManagerPermission只是java.security.Permission的一个简单实现,用于确保仅有授权的代码可以启用/禁用安全管理器。它看起来像这样:

import java.security.Permission;

public class ToggleSecurityManagerPermission extends Permission {

  private static final long serialVersionUID = 4812713037565136922L;
  private static final String NAME = "ToggleSecurityManagerPermission";

  public ToggleSecurityManagerPermission() {
    super(NAME);
  }

  @Override
  public boolean implies(Permission permission) {
    return this.equals(permission);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj instanceof ToggleSecurityManagerPermission) {
      return true;
    }
    return false;
  }

  @Override
  public int hashCode() {
    return NAME.hashCode();
  }

  @Override
  public String getActions() {
    return "";
  }

}

4
引用您自己的来源:http://alphaloop.blogspot.com/2014/08/a-per-thread-java-security-manager.html 和 https://github.com/alphaloop/selective-security-manager。 - ziesemer
非常聪明地使用ThreadLocal,使系统范围的SecurityManagers有效地成为线程范围(大多数用户都希望如此)。还可以考虑使用InheritableThreadLocal自动将不允许的属性传输到由不受信任的代码生成的线程。 - Nick

4
很晚了,虽然我没有什么建议或解决方案,但我也遇到了类似的问题,更加研究性质。基本上,我试图为Java课程在电子学习平台上提供编程作业的自动评估和规定。
以下是一种方法:
  1. 创建单独的虚拟机(不是JVM),但对于每个学生,使用尽可能小的配置OS。
  2. 为Java或其他编程语言安装JRE或库,以便您希望学生在这些机器上编译和执行。
我知道这听起来很复杂,需要完成很多任务,但是Oracle Virtual Box已经提供了Java API来动态创建或克隆虚拟机。 https://www.virtualbox.org/sdkref/index.html (请注意,即使VMware也提供了相同的API)
对于最小大小和配置Linux发行版,您可以参考这里:http://www.slitaz.org/en/
因此,现在如果学生搞砸或尝试使用内存或文件系统或网络、套接字等,他最多只能损坏自己的VM。
您还可以在这些VM内部提供额外的安全性,例如为Java提供沙箱(安全管理器)或在Linux上创建用户特定帐户,从而限制访问。
希望这有所帮助!

1

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