Friday, August 7, 2009

Java Scripting API Sandbox

Well, I am adding generic scripting to my DarkMMO client. 

The server passes the name of the scripting language being used and then the script code.  The client finds the script engine and then executes the code using the Java Scripting API that is built into JDK 6.

This was all pretty straight forward and easy, however if I let the client execute any code the server sent it coudl be a BIG security hole.  Therefor I decided to try to sandbox the code.  It has been a frustrating few days but I posted a request for help to users@scripting.dev.java.net and got back a very useful link froma helpful soul there.

From that I was able to figure out how to do it, so here is the result. A generic Java Scripting Sandbox!  (Code below)  This sandbox is intiialized with the name of the scripting engine you want to use and then can be set to run any code you pass it with any permission set you chose,. In fact, the permission set can be changed on the fly by the main program.

In order for this to work, you need to have a security manager enabled.  I do this in DarkMMO with the VM argument -Djava.security.manager

Once you enable the security manager you have to give the code of the program and libraries it depends on permissions or it wont work.  I do that with a security.policy file that grants all permissions to the app like so:

grant {
    permission java.security.AllPermission;
};

Finally, I tell the VM to use that security.policy file with this VM argument:
-Djava.security.policy=security.policy
  

With that in place, the following class runs scripts in a sandbox.  All it handles is eval(String) because that's the only kind of script I need to handle, but its extension to the other kinds of evals aught to be intuitively obvious.

import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.Permission;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.Collection;

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class ScriptSandbox {
    ScriptEngine _scriptEngine;
    AccessControlContext _accessControlContext;
   
    public ScriptSandbox(String engineShortName) throws InstantiationException{
         ScriptEngineManager sem = new ScriptEngineManager();
         _scriptEngine = sem.getEngineByName("groovy");
         if (_scriptEngine==null){
             throw new InstantiationException("Could not load script engine: "+
                     engineShortName);
         }
         setPermissions(null);
    }
   
    public void setPermissions(Collection permissionCollection){
        Permissions perms = new Permissions();
        perms.add(new RuntimePermission("accessDeclaredMembers"));
        if (permissionCollection!=null){
            for (Permission p : permissionCollection){
                perms.add(p);
            }
        }
        // Cast to Certificate[] required because of ambiguity:
         ProtectionDomain domain = new ProtectionDomain(
                 new CodeSource( null, (Certificate[]) null ), perms );
         _accessControlContext = new AccessControlContext(
                 new ProtectionDomain[] { domain } );
    }
   
    public Object eval(final String code){
        return AccessController.doPrivileged(new PrivilegedAction(){
            @Override
            public Object run() {
                try {
                    return _scriptEngine.eval(code);
                } catch (ScriptException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                return null;
            }}, _accessControlContext);
    }  
}


2 comments:

Daniel Nägele said...

This is really helpful, thank you. I am wondering how you got Groovy to work with that approach though. I'm trying to code a project with various scripting engines embedded, and the only engine working using your approach to security is the native Rhino JS. You didn't do anything fancy to get Groovy to work other than adding a jar to the build path, did you?

SDI said...

How do you prevent the script from elevating its permissions with AccessController.doPrivileged()?