48

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 different (untrusted) users. The scripts should only be able to access a Java Object and those returned by the defined members. By default the scripts could instantiate any class using Class.forName() (using .getClass() of my supplied object). Is there any way to prohibit access to any java class not explicitly specified by me?

tom_ma
  • 598
  • 1
  • 5
  • 8
  • This is a really great question and one that will be more and more asked. I wish someone would gather all the facts/data/code/samples/answers/etc into a blog or something. In addition to sandboxing JS code in Java, more advanced topics such as how do you prevent someone from running an endless JS loop to sabotage the execution. In other words, how do you insert an execution watchdog in the third-party JS being executed. Anyway, thanks for the question! – Jeach Aug 05 '16 at 17:41
  • You should probably take a look at this too: https://stackoverflow.com/a/48259901/1035398 – Patrick M Jan 15 '18 at 09:28

9 Answers9

26

I asked this question on the Nashorn mailing list a while back:

Are there any recommendations for the best way to restrict the classes that Nashorn scripts can create to a whitelist? Or is the approach the same as any JSR223 engine (custom classloader on the ScriptEngineManager constructor)?

And got this answer from one of the Nashorn devs:

Hi,

  • Nashorn already filters classes - only public classes of non-sensitive packages (packages listed in package.access security property aka 'sensitive'). Package access check is done from a no-permissions context. i.e., whatever package that can be accessed from a no-permissions class are only allowed.

  • Nashorn filters Java reflective and jsr292 access - unless script has RuntimePermission("nashorn.JavaReflection"), the script wont be able to do reflection.

  • The above two require running with SecurityManager enabled. Under no security manager, the above filtering won't apply.

  • You could remove global Java.type function and Packages object (+ com,edu,java,javafx,javax,org,JavaImporter) in global scope and/or replace those with whatever filtering functions that you implement. Because, these are the only entry points to Java access from script, customizing these functions => filtering Java access from scripts.

  • There is an undocumented option (right now used only to run test262 tests) "--no-java" of nashorn shell that does the above for you. i.e., Nashorn won't initialize Java hooks in global scope.

  • JSR223 does not provide any standards based hook to pass a custom class loader. This may have to be addressed in a (possible) future update of jsr223.

Hope this helps,

-Sundar

Kong
  • 7,648
  • 14
  • 58
  • 89
  • 4
    You can pass `--no-java` (and other options) to the engine with the following: `final ScriptEngine engine = new NashornScriptEngineFactory().getScriptEngine(new String[] { "--no-java" });` – ach Aug 12 '14 at 19:54
  • 1
    Thank you, but could you share your reference for this switch ? – gsimard Aug 14 '14 at 22:32
  • 2
    I just wanted to comment that the fourth method won't work as long as loadWithNewGlobal is not also removed. For example, I can recreate the Java object with this code : loadWithNewGlobal({script: "arguments[0].Java = Java", name: "exploit"}, this) – flowx1710 Dec 15 '14 at 10:12
  • @gsimard - I was wondering the same, and found http://hg.openjdk.java.net/jdk8/jdk8/nashorn/rev/eb7b8340ce3a - courtesy of http://stackoverflow.com/a/24468398/751158 . See also: https://wiki.openjdk.java.net/display/Nashorn/Nashorn+extensions . – ziesemer Apr 12 '15 at 19:41
20

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
Community
  • 1
  • 1
mkobit
  • 34,772
  • 9
  • 135
  • 134
9

I've researched ways of allowing users to write a simple script in a sandbox that is allowed access to some basic objects provided by my application (in the same way Google Apps Script works). My conclusion was that this is easier/better documented with Rhino than with Nashorn. You can:

  1. Define a class-shutter to avoid access to other classes: http://codeutopia.net/blog/2009/01/02/sandboxing-rhino-in-java/

  2. Limit the number of instructions to avoid endess-loops with observeInstructionCount: http://www-archive.mozilla.org/rhino/apidocs/org/mozilla/javascript/ContextFactory.html

However be warned that with untrusted users this is not enough, because they can still (by accident or on purpose) allocate a hugh amount of memory, causing your JVM to throw an OutOfMemoryError. I have not found a safe solution to this last point yet.

Tomas
  • 455
  • 1
  • 6
  • 11
  • 2
    the question was how to secure Nashorn, not Rhino – stryba May 05 '14 at 07:49
  • 1
    That's why I said "My conclusion was that this is easier/better documented with Rhino than with Nashorn." Both achieve similar goals and Rhino is easier to lock down so @tom_ma might be better off with Rhino. – Tomas May 05 '14 at 13:52
  • 1
    Rhino and Nashorn both execute JS. Thats about where the similarities end! – Kong Jun 17 '14 at 20:45
  • I currently had the same question to lock down JS-execution and using Rhino was the only valid solution (running Java 6). As said in the answer "observeInstructionCount" will not work in all cases: it will prevent infinite loops but doesnt work to detect endless-recursion (which will end up in an OutOfMemory). The only way to get a chain onto recursion was to limit the StackSize, which you can do via c.ontext.setMaximumInterpreterStackDepth(500). – lostiniceland Nov 30 '16 at 11:58
7

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!

mxro
  • 3,630
  • 3
  • 27
  • 32
  • Wow, I should have looked at your post before commenting on while-loops limitations above. I read the main page of the project on the GitHub site and it seems very promising. Things like the CPU limitations are amazing if they work well. I'll definitely be reading the code and testing it for sure. Thanks a lot for the post!! – Jeach Aug 05 '16 at 17:54
  • I need to make a Javascript parser that can parse: maths, boolean queries and bitwise manipulation, but pretty much nothing else. Is it possible to limit an eval in this way? Any ideas what I would need to allow in exposeToScripts? – Perry Monschau Aug 07 '16 at 13:24
  • 1
    @perry-monschau : Have a look at the Nashorn Sandbox GitHub project linked above. The default there only allows bare-bone JavaScript features. However, it is more than what you ask for: for instance string manipulation will work and I wouldn't know of any way to prevent this in any JS parser. Hope this helps. – mxro Aug 08 '16 at 02:44
  • `Class.forName`? How can you block that? – TheRealChx101 Jul 26 '18 at 09:56
  • 1
    @TheRealChx101 Class.forName will always be blocked when there is a ClassFilter. The sandbox also blocks access to Java.type(''). See some examples [here](https://github.com/javadelight/delight-nashorn-sandbox/blob/0f1fb573c0680e8c2e0e619c59a469aa8ec34a62/src/test/java/delight/nashornsandbox/TestClassForName.java) – mxro Jul 27 '18 at 21:50
6

So far as I can tell, you can't sandbox Nashorn. An untrusted user can execute the "Additional Nashorn Built-In Functions" listed here:

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

which include "quit()". I tested it; it exits the JVM entirely.

(As an aside, in my setup the global objects, $ENV, $ARG, did not work, which is good.)

If I'm wrong about this, someone please leave a comment.

ccleve
  • 13,099
  • 21
  • 76
  • 137
  • 1
    It says "You can enable shell scripting extensions in Nashorn using the jjs command with the -scripting option". So what if just NOT to enable? And, in general, the article seems about some standalone command line tool. – Audrius Meskauskas Nov 20 '15 at 20:06
  • 1
    Most of the listed functions are also available without scripting extensions, this includes `quit`. But it seems that simply prefixing the user-provided script with a preambel like the following should get rid of the problem: `var quit=function(){throw 'Unsupported operation: quit';};var exit=function(){throw 'Unsupported operation: exit';};` I'd also disable the `print` and most other global functions, because they can also be disruptive or lead to other security problems (e.g. `load` could be used to test for file existence on the server). – Christian Semrau Apr 14 '17 at 17:03
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

0

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)

Amadan
  • 169,219
  • 18
  • 195
  • 256
0

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.

Edu Garcia
  • 403
  • 7
  • 20
0

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

zezuha
  • 21
  • 6