Sandbox针对Java应用程序中的恶意代码

在允许用户提交自己的代码以便服务器运行的模拟服务器环境中,对于任何用户提交的代码在沙箱侧运行显然是有利的,与小应用程序在浏览器内不同。 我希望能够利用JVM本身,而不是添加另一个VM层来隔离这些提交的组件。

这种限制似乎可以使用现有的Java沙箱模型,但是有没有一种dynamic的方式来使得运行应用程序的用户提交的部分成为可能?

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

  2. 在该线程上设置一个SecurityManager 。 创build一个SecurityManager的子类,它覆盖checkPermission(Permission perm) ,只为所有的权限抛出一个SecurityException ,除了less数select。 这里有一个方法列表和他们需要的权限: Java TM 6 SDK中的权限 。

  3. 使用自定义ClassLoader加载不受信任的代码。 你的类加载器将被调用所有不受信任的代码使用的类,所以你可以做一些事情,比如禁止访问单独的JDK类。 要做的事情是有一个允许的JDK类的白名单。

  4. 您可能想要在单独的JVM中运行不受信任的代码。 虽然前面的步骤会使代码安全,但是隔离代码仍然可以做一件令人讨厌的事情:尽可能多地分配内存,这会导致主应用程序的明显占用空间增大。

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

这是一个相当详细的话题,而且我主要是把这一切写下来。

但是,无论如何,一些不完美的,使用自己的风险,可能是错误的(伪)代码:

类加载器

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 } } 

安全pipe理器

 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) {} } } 

显然这样的scheme引发了各种各样的安全问题。 Java有一个严格的安全框架,但不是微不足道的。 不可忽视的一个问题是,如何让一个无特权的用户访问重要的系统组件成为可能。

抛开这个警告,如果你以源代码的forms接受用户input,你首先需要做的就是把它编译成Java字节码。 AFIAK,这不能在本地完成,所以您需要对javac进行系统调用,并将源代码编译为磁盘上的字节码。 这里有一个教程可以作为一个起点。 编辑 :正如我在评论中学到的,你实际上可以使用javax.tools.JavaCompiler从源代码本地编译Java代码

一旦拥有JVM字节码,就可以使用ClassLoader的 defineClass函数将其加载到JVM中。 要为此加载的类设置安全上下文,您需要指定一个ProtectionDomain 。 ProtectionDomain的最小构造函数需要一个CodeSource和一个PermissionCollection 。 PermissionCollection是您在这里主要使用的对象,您可以使用它来指定加载的类所具有的确切权限。 这些权限应该最终由JVM的AccessController强制执行。

这里有很多可能的错误点,在执行任何事情之前,您应该非常小心地完全理解所有的东西。

Java-Sandbox是一个使用有限权限集执行Java代码的库。 它可以用来只允许访问一组白名单和资源。 它似乎没有能力限制访问个别方法。 它使用一个带有自定义类加载器和安全pipe理器的系统来实现这一点。

我没有使用它,但它看起来很好devise和合理的文件。

@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()); 

那么提出任何build议或解决scheme都为时已晚,但我仍然面临类似的问题,更多的研究导向。 基本上,我试图为在线学习平台的Java课程的编程作业提供一个规定和自动评估。

  1. 一种方法可能是,为每个学生创build一个单独的虚拟机(不是JVM),而是具有最低可configuration操作系统的实际虚拟机。
  2. 根据您的编程语言安装适用于Java或库的JRE,无论您希望学生在这些机器上进行编译和执行。

我知道这听起来相当复杂和很多任务,但Oracle Virtual Box已经提供了Java API来dynamic创build或克隆虚拟机。 https://www.virtualbox.org/sdkref/index.html (请注意,即使VMware也提供相同的API)

而对于Linux发行版的最小尺寸和configuration,你可以在这里参考这个http://www.slitaz.org/en/

所以现在,如果学生弄坏或试图做到这一点,可能是与内存或文件系统或networking,套接字,最大限度地,他可以损坏自己的虚拟机。

同样在这些虚拟机的内部,您可以提供额外的安全性,例如Java的Sandbox(安全pipe理器)或在Linux上创build用户特定的帐户,从而限制访问。

希望这可以帮助 !!

这是一个线程安全的解决scheme:

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. Eg, 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 (eg 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

阿诺

为了解决接受的答案中的问题,自定义的SecurityManager将应用于JVM中的所有线程,而不是以每个线程为基础,您可以创build一个自定义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的简单实现,以确保只有授权的代码才能启用/禁用安全pipe理器。 它看起来像这样:

 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 ""; } } 

您可能需要使用自定义SecurityManger和/或AccessController 。 有关详细信息,请参阅Sun的Java安全体系结构和其他安全性文档 。