如何创建安全的JEXL(脚本)沙箱?

时间:2011-07-01 17:09:01

标签: java jexl

我正在为JEXL脚本创建一个沙箱来执行,以便恶意用户无法访问我们允许他们访问的变量之外的数据,也无法在服务器上执行DOS攻击。我想为其他人做这方面的记录,也可以让其他人参与这个方法。

以下列出了我需要注意的事项:

  1. 仅允许使用白名单中的“新”实例化类。
  2. 不允许在任何类上访问getClass方法,因为可以调用forName并且可以访问任何类。
  3. 限制对文件等资源的访问。
  4. 只允许表达式执行一段时间,以便我们可以限制它消耗的资源量。
  5. 这不适用于JEXL,但可能适用于您正在使用的脚本语言:

    1. 不允许对象拥有自定义的finalize方法,因为finalize方法是从终结器线程调用的,并且将使用原始的AccessControlContext而不是用于创建对象并在其中执行代码的方法执行。

1 个答案:

答案 0 :(得分:8)

更新:这都是使用JEXL 2.0.1完成的。您可能需要对其进行调整以使其适用于较新版本。

以下是处理这些案例的方法。我已经创建了单元测试来测试这些案例,我已经证实它们有效。

  1. JEXL让这很容易。只需创建一个自定义ClassLoader。覆盖两个loadClass()方法。在JexlEngine上调用setClassLoader()。

  2. 再一次,JEXL让这很容易。你必须阻止'.class'和'.getClass()'。创建自己的Uberspect类,扩展UberspectImpl。覆盖getPropertyGet,如果identifier等于“class”则返回null。覆盖getMethod,如果方法等于“getClass”,则返回null。构建JexlEngine时,会传递对Uberspect实现的引用。

    class MyUberspectImpl extends UberspectImpl {
    
        public MyUberspectImpl(Log jexlLog) {
            super(jexlLog);
        }
    
        @Override
        public JexlPropertyGet getPropertyGet(Object obj, Object identifier, JexlInfo info) {
            // for security we do not allow access to .class property
            if ("class".equals(identifier)) throw new RuntimeException("Access to getClass() method is not allowed");
            JexlPropertyGet propertyGet = super.getPropertyGet(obj, identifier, info);
            return propertyGet;
        }
    
        @Override
        public JexlMethod getMethod(Object obj, String method, Object[] args, JexlInfo info) {
            // for security we do not allow access to .getClass() method
            if ("getClass".equals(method)) throw new RuntimeException("Access to getClass() method is not allowed");
            return super.getMethod(obj, method, args, info);
        }
    
    }
    
  3. 您可以使用Java的AccessController机制执行此操作。我会尽快做到这一点。使用-Djava.security.policy = policyfile启动java。创建一个名为policyfile的文件,其中包含以下行: grant {permission java.security.AllPermission; }; 使用此调用设置默认的SecurityManager:System.setSecurityManager(new SecurityManager());现在您可以控制权限,默认情况下您的应用具有所有权限。如果您将应用程序的权限限制为当然只需要它,那会更好。接下来,创建一个AccessControlContext,将权限限制在最低限度,并调用AccessController.doPrivileged()并传递AccessControlContext,然后在doPrivileged()中执行JEXL脚本。这是一个小程序,用于演示这一点。 JEXL脚本调用System.exit(1),如果它没有包装在doPrivileged()中,它将成功终止JVM。

    System.out.println("java.security.policy=" + System.getProperty("java.security.policy"));
    System.setSecurityManager(new SecurityManager());
    try {
        Permissions perms = new Permissions();
        perms.add(new RuntimePermission("accessDeclaredMembers"));
        ProtectionDomain domain = new ProtectionDomain(new CodeSource( null, (Certificate[]) null ), perms );
        AccessControlContext restrictedAccessControlContext = new AccessControlContext(new ProtectionDomain[] { domain } );
    
        JexlEngine jexlEngine = new JexlEngine();
        final Script finalExpression = jexlEngine.createScript(
                "i = 0; intClazz = i.class; "
                + "clazz = intClazz.forName(\"java.lang.System\"); "
                + "m = clazz.methods; m[0].invoke(null, 1); c");
    
        AccessController.doPrivileged(new PrivilegedExceptionAction<Object>() {
            @Override
            public Object run() throws Exception {
                return finalExpression.execute(new MapContext());
            }
        }, restrictedAccessControlContext);
    }
    catch (Throwable ex) {
        ex.printStackTrace();
    }
    
  4. 这样做的诀窍是在脚本完成之前中断脚本。我发现这样做的一种方法是创建一个自定义的JexlArithmetic类。然后覆盖该类中的每个方法,并在调用超类中的实际方法之前检查脚本是否应该停止执行。我正在使用ExecutorService来创建线程。当调用Future.get()时,传递等待的时间量。如果抛出TimeoutException,则调用Future.cancel(),它会中断运行脚本的Thread。在新的JexlArithmetic类中的每个重写方法内部检查Thread.interrupted(),如果为true则抛出java.util.concurrent.CancellationException。 是否有更好的位置来放置代码,这些代码会在脚本执行时定期执行,以便可以中断?

  5. 以下是MyJexlArithmetic类的摘录。您必须添加所有其他方法:

        public class MyJexlArithmetic extends JexlArithmetic {
    
            public MyJexlArithmetic(boolean lenient) {
                super(lenient);
            }
    
            private void checkInterrupted() {
                if (Thread.interrupted()) throw new CancellationException();
            }
    
            @Override
            public boolean equals(Object left, Object right) {
                checkInterrupted();
                return super.equals(left, right); //To change body of generated methods, choose Tools | Templates.
            }
    
            @Override
            public Object add(Object left, Object right) {
                checkInterrupted();
                return super.add(left, right);
            }
        }
    

    以下是我实例化JexlEngine的方法:

            Log jexlLog = LogFactory.getLog("JEXL");
            Map <String, Object> functions = new HashMap();
            jexlEngine = new JexlEngine(new MyUberspectImpl(jexlLog), new MyJexlArithmetic(false), functions, jexlLog);