Infrequently updated blog!

Things Calum may be responsible for

Simple JVM sandboxing

Sunday, 22 June 2008

I have written an dumb IRC bot which (among other things) can run scripts in arbitrary languages, provided they have Java implementations wrapped by the JSR-223 scripting API (a.k.a. javax.script in Java 6). At the moment I've got it doing Ruby, Groovy, Javascript and Scheme.

As part of this I decided that I needed to security restrict the script runner, since by design people will essentially be able to run arbitrary code in it. Java is so kind as to provide this sort of sandboxing functionality in its core framework (this is the same sort of security protection offered when you ran Java as an applet on a webpage years ago when applets were still cool), and although it's very well documented on a high-level online, I found it difficult to find a low-level explanation on exactly how to use the code to build a sandbox like I wanted, so here's how I ended up doing it.

It's worth noting that my IRC bot is actually written in Scala, so the actual Java code here (translated) is probably not well-tested. I've put the Scala wrapper I made at the foot of the entry in case it's useful to anyone.

First step – enable a SecurityManager

The first thing to realise (I certainly didn't) is that none of the security infrastructure in Java does anything until you enable it. By default the security policy checks are disabled (presumably for performance reasons); to enable them you need to attach an instance of SecurityManager to the system. You only need to do this once, and I'd recommend doing it as part of your app setup. It's really easy to do:

System.setSecurityManager(new SecurityManager());

The important thing to notice is that the current security policy — as defined by your .java.policy file in your home directory (I believe this is the same on Windows, incidentally), which will fall back to a very restrictive policy (the one used for applets) if the file doesn't exist. Now really I'd like any code that resides on my machine to run without sandboxing even when a security manager is in place (I like to live dangerously), so I can enter the following into my /home/calum/.java.policy file:

grant codeBase "file:/home/calum/-" {
  permission java.security.AllPermission;
};

The /- at the end matches all files below that directory recursively. One can also use /* to match all JAR and class files in a directory, / to match only class files, and specify individual files. For more information on policy files, check out this tasty document.

Important: Once you create this file, it overrides the default Java policy for anything you have a rule for. Do not just blanket grant permissions without a codeBase, you will be opening yourself to a world of hurt, in security terms.

Once you've restricted your JVM's permissions by adding a SecurityManager, then un-restricted them by granting the permissions back, you're ready to actually manipulate security on parts of code in your app.

Constructing an simple sandbox

Code can be invoked at modified privilege levels by the static AccessController.doPrivileged(PrivilegedAction,AccessControlContext) method. For this, we need to construct a PrivilegedAction<T> (essentially a Callable<T>) which encapsulates the logic to be run (if the logic throws an exception, you can use PrivilegedExceptionAction<T> instead). For our simple case we can fill this context with a single ProtectionDomain with a CodeSource (codesource can never be null) which is constructed will null values to indicate unsigned code from an unknown location, and a PermissionCollection consisting only of the permissions we'd like to grant our code (represented below as perms:

// Cast to Certificate[] required because of ambiguity:
ProtectionDomain domain = new ProtectionDomain( new CodeSource( null, (Certificate[]) null ), perms ); 
AccessControlContext context = new AccessControlContext( new ProtectionDomain[] { domain } );
T result = AccessController.doPrivileged(action, context);

Here's a full Java program which demonstrates the use of these methods (huge, I'm afraid — I've omitted the imports for brevity):

public class SandboxTest {
 private static final File file = new File( "/etc/motd" );
 
 public static void main(String[] args) throws Exception
 {
  // Do not forget this line!
  System.setSecurityManager( new SecurityManager() );
  
  System.out.println( "Calling method directly:" );
  
  System.out.println( readFirstFileLine(file).run() );
  
  System.out.println( "Calling method in restrictive sandbox:" );
  
  System.out.println( callInSandbox( new Permissions() ) );
  
  System.out.println( "Calling method with file read permission:" );
  
  Permissions perms = new Permissions();
  perms.add( new FilePermission( "/etc/motd", "read" ) );
  
  System.out.println( callInSandbox(perms) );
 }
 
 private static String callInSandbox( PermissionCollection perms )
 {
  ProtectionDomain domain = new ProtectionDomain( new CodeSource( null, (Certificate[]) null ), perms );
  AccessControlContext context = new AccessControlContext( new ProtectionDomain[] { domain } );
  try
  {
   return AccessController.doPrivileged( readFirstFileLine(file), context );
  }
  catch( Exception e )
  {
   return e.toString();
  }
 }
 
 private static PrivilegedExceptionAction<String> readFirstFileLine( final File file )
 {
  return new PrivilegedExceptionAction<String>() 
  {
   @Override
   public String run() throws Exception {
    BufferedReader reader = new BufferedReader( new FileReader( file ) );
    
    try
    {
     return reader.readLine();
    }
    finally
    {
     reader.close();
    }
   }
  };
 }
}

On my machine, this yields:

Calling method directly:
Linux thoth 2.6.24-19-generic #1 SMP Wed Jun 4 16:35:01 UTC 2008 i686
Calling method in restrictive sandbox:
java.security.AccessControlException: access denied (java.io.FilePermission /etc/motd read)
Calling method with file read permission:
Linux thoth 2.6.24-19-generic #1 SMP Wed Jun 4 16:35:01 UTC 2008 i686

Touch of Scala

Since I developed my app in Scala, here's a little object I built for simplifying this process. It works similarly to the callInSandbox method above, but also wraps Scala functions so you can use closures pretty simply:

import java.security._
import java.net.URL

object Security {
  private val codeSource = new CodeSource( null: URL, null: Array[cert.Certificate] )

  def sandboxed[T]( perms: PermissionCollection )( f: => T): T = {
    val domain = new ProtectionDomain( codeSource, perms )
    val context = new AccessControlContext( Array( domain ) )

    AccessController.doPrivileged( new Action( f ), context )
  }

  private class Action[T]( f: => T ) extends PrivilegedAction[T] {
    override def run() = f
  }
}

In order to use this, construct your Permissions that you want to grant to the item, import Security.sandboxed, and just wrap like this:

sandboxed( perms ) {
  println( "This code is running within the sandbox!" )
}

I hope this helps someone out!

Labels: , , ,