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

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

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

【问题讨论】:

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

标签: java javascript nashorn


【解决方案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的更新。

希望这会有所帮助,

-桑达尔

【讨论】:

【解决方案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 更容易/更好地记录这一点。你可以:

    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。对于最后一点,我还没有找到安全的解决方案。

    【讨论】:

    • 问题是如何保护 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讨论过

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-07-20
            • 2013-06-15
            • 2010-11-20
            相关资源
            最近更新 更多