安全的 Nashorn JS 执行

Posted

技术标签:

【中文标题】安全的 Nashorn JS 执行【英文标题】:Secure Nashorn JS Execution 【发布时间】:2014-01-14 14:19:22 【问题描述】:

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

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

【问题讨论】:

这是一个非常棒的问题,而且会被越来越多的人问到。我希望有人将所有事实/数据/代码/样本/答案/等收集到博客或其他东西中。除了在 Java 中对 JS 代码进行沙箱处理之外,还有更高级的主题,例如如何防止某人运行无休止的 JS 循环来破坏执行。也就是说,如何在正在执行的第三方 JS 中插入执行看门狗。无论如何,谢谢你的问题! 你也应该看看这个:***.com/a/48259901/1035398 【参考方案1】:

我不久前asked this question on the Nashorn mailing list:

有没有什么最好的方法推荐 将 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”,可以为您执行上述操作。 IE。, Nashorn 不会在全局范围内初始化 Java 挂钩。

JSR223 不提供任何基于标准的挂钩来传递自定义类加载器。这可能必须在(可能的)未来解决 jsr223的更新。

希望这会有所帮助,

-桑达尔

【讨论】:

您可以将--no-java(和其他选项)通过以下方式传递给引擎:final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new String[] "--no-java" ); 谢谢,但你能分享一下你对这个开关的参考吗? 我只是想评论一下,只要不删除 loadWithNewGlobal,第四种方法就行不通。例如,我可以使用以下代码重新创建 Java 对象: loadWithNewGlobal(script: "arguments[0].Java = Java", name: "exploit", this) @gsimard - 我也想知道,然后找到了hg.openjdk.java.net/jdk8/jdk8/nashorn/rev/eb7b8340ce3a - 由***.com/a/24468398/751158 提供。另见:wiki.openjdk.java.net/display/Nashorn/Nashorn+extensions。【参考方案2】:

1.8u40中添加,您可以使用ClassFilter 来限制引擎可以使用的类。

这是来自Oracle documentation 的示例:

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

此示例打印以下内容:

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

【讨论】:

【参考方案3】:

我研究了允许用户在沙箱中编写简单脚本的方法,该脚本允许访问我的应用程序提供的一些基本对象(与Google Apps Script 的工作方式相同)。我的结论是,使用 Rhino 比使用 Nashorn 更容易/更好地记录这一点。你可以:

    定义一个类快门以避免访问其他类:http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/

    使用 observeInstructionCount 限制指令数量以避免结束循环:http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html

但是请注意,对于不受信任的用户,这还不够,因为他们仍然可以(意外或故意)分配大量内存,从而导致您的 JVM 抛出 OutOfMemoryError。对于最后一点,我还没有找到安全的解决方案。

【讨论】:

问题是如何保护 Nashorn,而不是 Rhino 这就是为什么我说“我的结论是,使用 Rhino 比使用 Nashorn 更容易/更好地记录这一点。”两者都实现了相似的目标,而且 Rhino 更容易锁定,所以 @tom_ma 使用 Rhino 可能会更好。 Rhino 和 Nashorn 都执行 JS。这就是相似之处的结束! 我目前有相同的问题来锁定 JS 执行,并且使用 Rhino 是唯一有效的解决方案(运行 Java 6)。正如答案中所说,“observeInstructionCount”并非在所有情况下都有效:它可以防止无限循环,但不能检测无限递归(最终会出现 OutOfMemory)。使链进入递归的唯一方法是限制 StackSize,您可以通过 c.ontext.setMaximumInterpreterStackDepth(500) 来实现。【参考方案4】:

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

按照Oracle Nashorn Docs中的示例:

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

我今天在一个小型库中包含了其他一些措施:Nashorn Sandbox(在 GitHub 上)。享受吧!

【讨论】:

哇,我应该先看看你的帖子,然后再评论上面的 while-loops 限制。我在 GitHub 网站上阅读了该项目的主页,看起来很有希望。如果它们运行良好,那么 CPU 限制之类的东西就会令人惊叹。我肯定会阅读代码并肯定会对其进行测试。非常感谢您的帖子! 我需要制作一个可以解析的 Javascript 解析器:数学、布尔查询和按位操作,但几乎没有其他内容。是否可以以这种方式限制评估?在exposeToScripts中我需要允许什么想法? @perry-monschau :查看上面链接的 Nashorn Sandbox GitHub 项目。那里的默认设置只允许基本的 JavaScript 功能。但是,它超出了您的要求:例如,字符串操作将起作用,而且我不知道有任何方法可以在任何 JS 解析器中防止这种情况发生。希望这会有所帮助。 Class.forName?你怎么能阻止它? @TheRealChx101 Class.forName 在有 ClassFilter 时将始终被阻止。沙盒还阻止对 Java.type('') 的访问。查看一些示例here【参考方案5】:

据我所知,您无法对 Nashorn 进行沙盒处理。不受信任的用户可以执行此处列出的“其他 Nashorn 内置函数”:

https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/nashorn/shell.html

其中包括“quit()”。我测试了它;它完全退出 JVM。

(顺便说一句,在我的设置中,全局对象 $ENV、$ARG 不起作用,这很好。)

如果我错了,请发表评论。

【讨论】:

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

在 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。

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

【讨论】:

【参考方案7】:

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

(免责声明:我对较新的 Java 不是很熟悉,所以这个答案可能是老派/过时的)

【讨论】:

【参考方案8】:

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

我已经尝试过“Java 沙盒”(http://blog.datenwerke.net/p/the-java-sandbox.html),虽然它的边缘有点粗糙,但它确实有效。

【讨论】:

【参考方案9】:

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

在包含 Nashorn 的所有 Oracle Hotspot 版本中,人们都可以编写 JavaScript 来在这个 JVM 上执行任何 Java/JavaScript 代码。 自 2019 年 1 月起,Oracle 安全团队坚持使用安全管理器是强制性的。

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

【讨论】:

以上是关于安全的 Nashorn JS 执行的主要内容,如果未能解决你的问题,请参考以下文章

有没有哪个Java 8妙用Nashorn引擎的好例子

Java8 Nashorn实现Java调用javascript代码

Java 7 可执行的 Nashorn,取代 Rhino

Java 8 的 Nashorn 脚本引擎教程

Java新特性之Nashorn的实例详解

调试D2JS