安全Nashorn JS执行

时间:2013-12-27 00:25:17

标签: java javascript nashorn

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

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

9 个答案:

答案 0 :(得分:25)

asked this question on the Nashorn mailing list一会儿回来:

  

对于最好的方法,是否有任何建议   将Nashorn脚本可以创建的类限制为白名单?   或者方法与任何JSR223引擎(自定义类加载器)相同   在ScriptEngineManager构造函数上)?

从Nashorn开发者那里得到了这个答案:

  

您好,

     
      
  • Nashorn已经过滤了类 - 只有非敏感包的公共类(package.access security中列出的包)   财产又名'敏感')。包访问检查是从a   无权限上下文。即,可以访问的任何包   只允许来自无权限类。

  •   
  • Nashorn过滤Java反射和jsr292访问 - 除非脚本有RuntimePermission(“nashorn.JavaReflection”),脚本不会   能够做反思。

  •   
  • 以上两项要求在启用SecurityManager的情况下运行。在没有安全管理员的情况下,上述过滤将不适用。

  •   
  • 您可以在全局范围内删除全局Java.type函数和Packages对象(+ com,edu,java,javafx,javax,org,JavaImporter)和/或   用你实现的任何过滤功能替换那些。   因为,这些是从脚本访问Java的唯一入口点,   自定义这些函数=>从脚本过滤Java访问。

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

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

  •   
     

希望这有帮助,

     

-Sundar

答案 1 :(得分:17)

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

答案 2 :(得分:8)

我研究了允许用户在沙箱中编写一个简单脚本的方法,该沙箱允许访问我的应用程序提供的一些基本对象(以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

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

答案 3 :(得分:6)

据我所知,你不能沙箱Nashorn。不受信任的用户可以执行此处列出的“Additional Nashorn Built-In Functions”:

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

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

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

如果我对此有误,请有人留言。

答案 4 :(得分:5)

您可以非常轻松地创建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上)。享受!

答案 5 :(得分: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,而是在单独的进程中执行。

PS:这里通过反思加载Nashorn,但评论中也提供了等效的Java代码

答案 6 :(得分:0)

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

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

答案 7 :(得分:0)

如果您不想实现自己的ClassLoader&amp;可以使用外部沙箱库。 SecurityManager(现在是沙盒的唯一方法)。

我已经尝试过#Java; Java Sandbox&#34; (http://blog.datenwerke.net/p/the-java-sandbox.html)尽管它的边缘有点粗糙,但它有效。

答案 8 :(得分:0)

如果不使用Security Manager,就无法在Nashorn上安全地执行JavaScript。

在包括Nashorn在内的所有Oracle Hotspot版本中,都可以编写JavaScript,该JavaScript将在此JVM上执行任何Java / JavaScript代码。 从2019年1月起,Oracle安全团队坚持必须强制使用Security Manager。

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

中进行了讨论