如何创建一个安全的JEXL(脚本)沙盒?

6
我正在创建一个沙盒环境,用于执行JEXL脚本,以防止恶意用户访问我们未授权的数据,并且还可以防止对服务器进行DOS攻击。我想为任何其他使用此方法的人记录此过程,并获取其他人对此方法的建议。
以下是需要注意的事项列表:
1. 只允许实例化白名单中的类。 2. 不允许访问任何类的getClass方法,因为这样可以调用forName并访问任何类。 3. 限制对资源(如文件)的访问。 4. 仅允许表达式在特定时间内执行,以便我们可以限制其消耗的资源量。
以下内容不适用于JEXL,但可能适用于您正在使用的脚本语言:
1. 不允许对象具有自定义finalize方法,因为finalize方法是从终结器线程调用的,将使用原始AccessControlContext而不是用于创建对象和执行其中代码的上下文。
1个回答

10

更新:这些都是使用 JEXL 2.0.1 完成的。您可能需要根据新版本进行适应。

这是我处理每种情况的方法。我创建了单元测试来测试每种情况,已验证它们有效。

  1. JEXL 让这个变得非常简单。只需创建一个自定义 ClassLoader。覆盖两个 loadClass() 方法。在 JexlEngine 上调用 setClassLoader()。

  2. 同样,JEXL 让这个变得非常简单。您必须阻止 '.class' 和 '.getClass()'。创建自己的 Uberspect 类,它继承 UberspectImpl。重写 getPropertyGet,如果标识符等于 "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);
        }
    
    }
    
    你可以使用 Java 的 AccessController 机制来实现这个功能。我会简要介绍一下如何进行操作。在启动 Java 时加上参数 -Djava.security.policy=policyfile。创建一个名为 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();
    }
    
    这个问题的关键在于在脚本完成之前中断它。我发现一种方法是创建一个自定义的JexlArithmetic类,然后覆盖该类中的每个方法,在调用超类中的真实方法之前检查脚本是否应该停止执行。我使用ExecutorService创建线程。当调用Future.get()时传递等待时间。如果抛出TimeoutException,则调用Future.cancel()以中断运行脚本的线程。在新的JexlArithmetic类中的每个覆盖方法内检查Thread.interrupted(),如果为true,则抛出java.util.concurrent.CancellationException。
    是否有更好的位置来放置代码,使其可以作为正在执行脚本的定期执行的脚本被中断?以下是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);
    

你如何处理可能利用脚本进行恶意意图的 forName 情况?同时,Uberspect 也没有帮助。或者我使用它的方式不对。我创建了一个具有自定义 Uberspect 类的 JexlEngine,但它从未验证过 .class 的使用。 - yogsma
我会在我的实现中添加更多信息。此外,我确定我正在使用比你更旧的JEXL版本。 - Sarel Botha

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