安全的Nashorn JS执行

49

如何使用Java8 Nashorn安全地执行一些用户提供的JS代码?

该脚本为一些基于servlet的报告扩展了一些计算。该应用程序有许多不同的(不受信任的)用户。脚本只能访问Java对象和定义成员返回的对象。默认情况下,脚本可以使用Class.forName()实例化任何类(使用我提供的对象的.getClass())。有没有办法禁止访问我未明确指定的任何Java类?


这是一个非常好的问题,而且越来越多人会问到。我希望有人能够将所有的事实/数据/代码/示例/答案等收集到一个博客或其他地方。除了在Java中对JS代码进行沙盒处理之外,还有更高级的主题,例如如何防止某些人运行无限制的JS循环以破坏执行。换句话说,如何在正在执行的第三方JS中插入执行看门狗。总之,感谢您的提问! - Jeach
你可能也应该看一下这个:https://dev59.com/w4Hba4cB1Zd3GeqPUrPa#48259901 - Patrick M
9个回答

26

我在一段时间前在Nashorn邮件列表中提出了这个问题

有没有什么建议,可以将Nashorn脚本创建的类限制为白名单?还是与任何JSR223引擎的方法相同(在ScriptEngineManager构造函数上使用自定义类加载器)?

然后从Nashorn开发人员那里得到了这个答案:

你好,
Nashorn已经过滤了类 - 只有非敏感包(列在package.access安全属性中的包)的公共类。包访问检查是从无权限上下文完成的。即,只允许从无权限类可以访问的任何包。
Nashorn过滤Java反射和jsr292访问 - 除非脚本具有RuntimePermission("nashorn.JavaReflection"),否则脚本将无法进行反射。
以上两种需要启用SecurityManager才能运行。在没有安全管理器的情况下,以上过滤不适用。
您可以删除全局Java.type函数和Packages对象(+com、edu、java、javafx、javax、org、JavaImporter)以及/或者将其替换为您实现的任何过滤函数。因为这些是从脚本访问Java的唯一入口点,自定义这些函数=>从脚本过滤Java访问。
Nashorn shell有一个未记录的选项(目前仅用于运行test262测试)"--no-java",它可以为您执行上述操作。即,Nashorn不会在全局范围内初始化Java钩子。
JSR223没有提供任何基于标准的挂钩来传递自定义类加载器。这可能需要在jsr223的(可能的)未来更新中解决。
希望这可以帮助,
-Sundar

4
您可以使用以下方式将 --no-java(以及其他选项)传递给引擎:final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new String[] { "--no-java" }); - ach
1
谢谢,但您能分享一下这个开关的参考资料吗? - gsimard
2
我只想评论一下,只要不删除 loadWithNewGlobal,第四种方法就行不通。例如,我可以使用以下代码重新创建 Java 对象:loadWithNewGlobal({script: "arguments[0].Java = Java", name: "exploit"}, this)。 - flowx1710
@gsimard - 我也在想同样的事情,并找到了http://hg.openjdk.java.net/jdk8/jdk8/nashorn/rev/eb7b8340ce3a - 感谢https://dev59.com/FoHba4cB1Zd3GeqPMA1k#24468398。另请参阅:https://wiki.openjdk.java.net/display/Nashorn/Nashorn+extensions。 - ziesemer

21

1.8u40版本开始,您可以使用ClassFilter来限制引擎可以使用哪些类。

以下是Oracle文档中的示例:

import javax.script.ScriptEngine;
import jdk.nashorn.api.scripting.ClassFilter;
import jdk.nashorn.api.scripting.NashornScriptEngineFactory;
 
public class MyClassFilterTest {
 
  class MyCF implements ClassFilter {
    @Override
    public boolean exposeToScripts(String s) {
      if (s.compareTo("java.io.File") == 0) return false;
      return true;
    }
  }
 
  public void testClassFilter() {
 
    final String script =
      "print(java.lang.System.getProperty(\"java.home\"));" +
      "print(\"Create file variable\");" +
      "var File = Java.type(\"java.io.File\");";
 
    NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
 
    ScriptEngine engine = factory.getScriptEngine(
      new MyClassFilterTest.MyCF());
    try {
      engine.eval(script);
    } catch (Exception e) {
      System.out.println("Exception caught: " + e.toString());
    }
  }
 
  public static void main(String[] args) {
    MyClassFilterTest myApp = new MyClassFilterTest();
    myApp.testClassFilter();
  }
}

This example prints the following:

C:\Java\jre8
Create file variable
Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
java.io.File

9

我研究了允许用户在沙盒中编写简单脚本并允许访问应用程序提供的一些基本对象的方法(类似于Google Apps Script的工作方式)。我的结论是,使用Rhino比Nashorn更容易/更好地记录文档。您可以:

  1. 定义类关闭器以避免访问其他类:http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/

  2. 通过observeInstructionCount限制指令的数量,以避免无限循环:http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html

但是请注意,对于不受信任的用户,这还不够,因为他们仍然可以(无意或有意)分配大量内存,导致您的JVM抛出OutOfMemoryError。我尚未找到解决此问题的安全解决方案。


3
问题是如何保护Nashorn,而不是Rhino。 - stryba
1
这就是为什么我说“我的结论是使用Rhino比使用Nashorn更容易/文档更好。”两者都可以实现类似的目标,而Rhino更容易锁定,因此@tom_ma可能更适合使用Rhino。 - Tomas
1
Rhino和Nashorn都可以执行JS,但它们的相似之处就到此为止了! - Kong
我之前也曾有过同样的问题,需要锁定JS执行,并且使用Rhino是唯一有效的解决方案(运行Java 6)。正如答案中所述,“observeInstructionCount”并不适用于所有情况:它可以防止无限循环,但无法检测无限递归(这会导致OutOfMemory)。将链条置于递归状态的唯一方法是限制StackSize,您可以通过c.context.setMaximumInterpreterStackDepth(500)实现。 - lostiniceland

7

您可以很容易地创建一个ClassFilter,以对JavaScript中可用的Java类进行细粒度控制。

根据Oracle Nashorn文档中的示例:

class MyCF implements ClassFilter {
    @Override
    public boolean exposeToScripts(String s) {
      if (s.compareTo("java.io.File") == 0) return false;
      return true;
    }
}

今天我将这个以及其他一些措施封装成了一个小型库:Nashorn沙箱(在GitHub上)。享受吧!


哇,我在上面评论while循环限制之前应该看看你的帖子。我阅读了GitHub网站上项目的主页,它似乎非常有前途。如果CPU限制能够良好地工作,那么这样的事情是令人惊叹的。我一定会阅读代码并进行测试。非常感谢您的帖子! - Jeach
我需要制作一个JavaScript解析器,可以解析数学、布尔查询和位运算,但几乎不允许其他操作。有没有可能以这种方式限制eval?您有什么想法,我需要在exposeToScripts中允许哪些内容? - Perry Monschau
1
@perry-monschau:请查看上面链接的Nashorn Sandbox GitHub项目。默认情况下,它只允许基本的JavaScript功能。但是,这已经超出了您的要求:例如,字符串操作将起作用,我不知道任何JS解析器可以防止这种情况发生。希望这可以帮到您。 - mxro
Class.forName?你怎么能阻止它? - TheRealChx101
1
@TheRealChx101 当存在ClassFilter时,Class.forName将始终被阻止。沙箱还会阻止访问Java.type('')。在此处查看一些示例:(https://github.com/javadelight/delight-nashorn-sandbox/blob/0f1fb573c0680e8c2e0e619c59a469aa8ec34a62/src/test/java/delight/nashornsandbox/TestClassForName.java) - mxro

6
据我所知,您无法对Nashorn进行沙箱隔离。一个不受信任的用户可以执行此处列出的“附加Nashorn内置函数”:https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html,其中包括“quit()”。我测试过了,它将完全退出JVM。
(顺便说一下,在我的设置中,全局对象$ENV,$ARG没有起作用,这是好事。)
如果我对此有误,请留言指正。

1
它说:“您可以使用带有-scripting选项的jjs命令在Nashorn中启用shell脚本扩展”。那么,如果不启用呢?而且,总的来说,这篇文章似乎是关于一些独立的命令行工具。 - Audrius Meškauskas
1
大多数列出的函数也可以在没有脚本扩展的情况下使用,包括 quit。但是似乎只需在用户提供的脚本前加上以下类似的前缀即可解决问题:var quit=function(){throw 'Unsupported operation: quit';};var exit=function(){throw 'Unsupported operation: exit';}; 我还会禁用 print 和大多数其他全局函数,因为它们也可能会造成干扰或导致其他安全问题(例如,load 可以用于测试服务器上的文件是否存在)。 - Christian Semrau

2
在Nashorn中保护JS执行的最好方法是启用SecurityManager并让Nashorn拒绝关键操作。此外,您可以创建一个监视类来检查脚本执行时间和内存,以避免无限循环和OutOfMemory错误。如果您在没有设置SecurityManager的受限环境中运行它,则可以考虑使用Nashorn ClassFilter来拒绝对Java类的全部/部分访问。除此之外,您还必须覆盖所有关键JS函数(如quit()等)。请查看此函数以管理所有这些方面(内存管理除外):
public static Object javascriptSafeEval(HashMap<String, Object> parameters, String algorithm, boolean enableSecurityManager, boolean disableCriticalJSFunctions, boolean disableLoadJSFunctions, boolean defaultDenyJavaClasses, List<String> javaClassesExceptionList, int maxAllowedExecTimeInSeconds) throws Exception {
    System.setProperty("java.net.useSystemProxies", "true");

    Policy originalPolicy = null;
    if(enableSecurityManager) {
        ProtectionDomain currentProtectionDomain = this.getClass().getProtectionDomain();
        originalPolicy = Policy.getPolicy();
        final Policy orinalPolicyFinal = originalPolicy;
        Policy.setPolicy(new Policy() {
            @Override
            public boolean implies(ProtectionDomain domain, Permission permission) {
                if(domain.equals(currentProtectionDomain))
                    return true;
                return orinalPolicyFinal.implies(domain, permission);
            }
        });
    }
    try {
        SecurityManager originalSecurityManager = null;
        if(enableSecurityManager) {
            originalSecurityManager = System.getSecurityManager();
            System.setSecurityManager(new SecurityManager() {
                //allow only the opening of a socket connection (required by the JS function load())
                @Override
                public void checkConnect(String host, int port, Object context) {}
                @Override
                public void checkConnect(String host, int port) {}
            });
        }

        try {
            ScriptEngine engineReflex = null;

            try{
                Class<?> nashornScriptEngineFactoryClass = Class.forName("jdk.nashorn.api.scripting.NashornScriptEngineFactory");
                Class<?> classFilterClass = Class.forName("jdk.nashorn.api.scripting.ClassFilter");

                engineReflex = (ScriptEngine)nashornScriptEngineFactoryClass.getDeclaredMethod("getScriptEngine", new Class[]{Class.forName("jdk.nashorn.api.scripting.ClassFilter")}).invoke(nashornScriptEngineFactoryClass.newInstance(), Proxy.newProxyInstance(classFilterClass.getClassLoader(), new Class[]{classFilterClass}, new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if(method.getName().equals("exposeToScripts")) {
                            if(javaClassesExceptionList != null && javaClassesExceptionList.contains(args[0]))
                                return defaultDenyJavaClasses;
                            return !defaultDenyJavaClasses;
                        }
                        throw new RuntimeException("no method found");
                    }
                }));
                /*
                engine = new jdk.nashorn.api.scripting.NashornScriptEngineFactory().getScriptEngine(new jdk.nashorn.api.scripting.ClassFilter() {
                    @Override
                    public boolean exposeToScripts(String arg0) {
                        ...
                    }
                });
                */
            }catch(Exception ex) {
                throw new Exception("Impossible to initialize the Nashorn Engine: " + ex.getMessage());
            }

            final ScriptEngine engine = engineReflex;

            if(parameters != null)
                for(Entry<String, Object> entry : parameters.entrySet())
                    engine.put(entry.getKey(), entry.getValue());

            if(disableCriticalJSFunctions)
                engine.eval("quit=function(){throw 'quit() not allowed';};exit=function(){throw 'exit() not allowed';};print=function(){throw 'print() not allowed';};echo=function(){throw 'echo() not allowed';};readFully=function(){throw 'readFully() not allowed';};readLine=function(){throw 'readLine() not allowed';};$ARG=null;$ENV=null;$EXEC=null;$OPTIONS=null;$OUT=null;$ERR=null;$EXIT=null;");
            if(disableLoadJSFunctions)
                engine.eval("load=function(){throw 'load() not allowed';};loadWithNewGlobal=function(){throw 'loadWithNewGlobal() not allowed';};");

            //nashorn-polyfill.js
            engine.eval("var global=this;var window=this;var process={env:{}};var console={};console.debug=print;console.log=print;console.warn=print;console.error=print;");

            class ScriptMonitor{
                public Object scriptResult = null;
                private boolean stop = false;
                Object lock = new Object();
                @SuppressWarnings("deprecation")
                public void startAndWait(Thread threadToMonitor, int secondsToWait) {
                    threadToMonitor.start();
                    synchronized (lock) {
                        if(!stop) {
                            try {
                                if(secondsToWait<1)
                                    lock.wait();
                                else
                                    lock.wait(1000*secondsToWait);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                    if(!stop) {
                        threadToMonitor.interrupt();
                        threadToMonitor.stop();
                        throw new RuntimeException("Javascript forced to termination: Execution time bigger then " + secondsToWait + " seconds");
                    }
                }
                public void stop() {
                    synchronized (lock) {
                        stop = true;
                        lock.notifyAll();
                    }
                }
            }
            final ScriptMonitor scriptMonitor = new ScriptMonitor();

            scriptMonitor.startAndWait(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        scriptMonitor.scriptResult = engine.eval(algorithm);
                    } catch (ScriptException e) {
                        throw new RuntimeException(e);
                    } finally {
                        scriptMonitor.stop();
                    }
                }
            }), maxAllowedExecTimeInSeconds);

            Object ret = scriptMonitor.scriptResult;
            return ret;
        } finally {
            if(enableSecurityManager)
                System.setSecurityManager(originalSecurityManager);
        }
    } finally {
        if(enableSecurityManager)
            Policy.setPolicy(originalPolicy);
    }
}

该函数目前使用已弃用的Thread stop()。改进方法可以将JS不在Thread中执行,而是在单独的进程中执行。

PS:这里通过反射加载了Nashorn,但等效的Java代码也在注释中提供了。


0

如果不使用安全管理器,就无法在Nashorn上安全地执行JavaScript。

在包含Nashorn的所有Oracle Hotspot版本中,都可以编写JavaScript代码,在此JVM上执行任何Java/JavaScript代码。 截至2019年1月,Oracle安全团队坚持要求使用安全管理器。

其中一个问题已经在https://github.com/javadelight/delight-nashorn-sandbox/issues/73中讨论过了。


0

如果您不想实现自己的ClassLoader和SecurityManager(这是目前唯一的沙箱方法),可以使用外部沙箱库。

我尝试过“Java沙箱”(http://blog.datenwerke.net/p/the-java-sandbox.html),虽然它有点不太完善,但它可以使用。


0

我认为覆盖提供的类的类加载器是控制访问类的最简单方法。

(免责声明:我不太熟悉较新的Java,因此这个答案可能有点老派/过时)


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