48

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

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

4

9 回答 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 访问。

  • 有一个未记录的选项(现在仅用于运行 test262 测试)“--no-java”的 nashorn shell 可以为您完成上述操作。即,Nashorn 不会在全局范围内初始化 Java 挂钩。

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

希望这可以帮助,

-桑达

于 2014-05-25T00:43:43.130 回答
20

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

此示例打印以下内容:

C:\Java\jre8
Create file variable
Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
java.io.File
于 2015-11-16T15:48:28.247 回答
9

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

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

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

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

于 2014-03-08T11:40:04.867 回答
7

您可以很容易地创建一个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 上)。享受!

于 2015-08-07T05:05:48.087 回答
6

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

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

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

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

如果我对此有误,请有人发表评论。

于 2015-06-30T00:12:24.140 回答
2

The best way to secure a JS execution in Nashorn is to enable the SecurityManager and let Nashorn deny the critical operations. In addition you can create a monitoring class that check the script execution time and memory in order to avoid infinite loops and outOfMemory. In case you run it in a restricted environment without possibility to setup the SecurityManager, you can think to use the Nashorn ClassFilter to deny all/partial access to the Java classes. In addition to that you must overwrite all the critical JS functions (like quit() etc.). Have a look at this function that manage all this aspects (except memory management):

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

The function currently use the deprecated Thread stop(). An improvement can be execute the JS not in a Thread but in a separate Process.

PS: here Nashorn is loaded through reflexion but the equivalent Java code is also provided in the comments

于 2018-02-12T17:11:51.607 回答
0

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

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

于 2014-05-15T23:10:30.900 回答
0

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

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

于 2013-12-27T00:27:48.373 回答
0

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

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

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

于 2019-01-02T17:49:22.357 回答