Secure Nashorn JS Execution

前端 未结 9 1262
故里飘歌
故里飘歌 2020-12-04 12:07

How can I securely execute some user supplied JS code using Java8 Nashorn?

The script extends some computations for some servlet based reports. The app has many diff

相关标签:
9条回答
  • 2020-12-04 12:36

    You can quite easily create a ClassFilter which allows fine-grained control of which Java classes are available in JavaScript.

    Following the example from the Oracle Nashorn Docs:

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

    I have wrapped this an a few other measures in a small library today: Nashorn Sandbox (on GitHub). Enjoy!

    0 讨论(0)
  • 2020-12-04 12:36

    I'd say overriding the supplied class's classloader is easiest way to control access to classes.

    (Disclaimer: I'm not really familiar with newer Java, so this answer may be old-school/obsolete)

    0 讨论(0)
  • 2020-12-04 12:37

    Without the use of Security Manager it is not possible to securely execute JavaScript on Nashorn.

    In all releases of Oracle Hotspot that included Nashorn one can write JavaScript that will execute any Java/JavaScript code on this JVM. As of January 2019, Oracle Security Team insist that use of Security Manager is mandatory.

    One of the problems is already discussed in https://github.com/javadelight/delight-nashorn-sandbox/issues/73

    0 讨论(0)
  • 2020-12-04 12:38

    Added in 1.8u40, you can use the ClassFilter to restrict what classes the engine can use.

    Here is an example from the 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();
      }
    }
    

    This example prints the following:

    C:\Java\jre8
    Create file variable
    Exception caught: java.lang.RuntimeException: java.lang.ClassNotFoundException:
    java.io.File
    
    0 讨论(0)
  • 2020-12-04 12:39

    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

    0 讨论(0)
  • 2020-12-04 12:39

    An external sandbox library can be used if you don't want to implement your own ClassLoader & SecurityManager (that's the only way of sandboxing for now).

    I've tried "The Java Sandbox" (http://blog.datenwerke.net/p/the-java-sandbox.html) although it's a bit rough around the edges, but it works.

    0 讨论(0)
提交回复
热议问题