The java-sandbox is open source. The java-sandbox is available under the [LGPL 3.0].
In this version we have added support for the [reflective-sandcastle library (version 0.1)]. The reflective-sandcastle is an enhancement to java's permission checking when using reflection. Using the reflective-sandcastle together with the sandbox better protects against breakout attempts via reflection.
In version 0.2 we have added many new features to the sandbox the most prominent of which are the wrapping of sandboxes into threads and running sandboxes on remote jvm agents (i.e., executing the sandboxed code on a completely separated java virtual machine).
Other new features include:
We start with a basic description on how the java-sandbox works. We then go through the various possibilities of creating sandboxes step by step. Besides this documentatin have a look at the [API documentation] and the accompanying tutorials:
Before we get into using the java-sandbox we need some background on how the java-sandbox works. For this it helps to understand the basics of java's SecurityManager and permission concept but it is not really necessary. However, to properly use the java-sandbox some knowledge about class loading in java is important.
The java-sandbox basically consists of two components (an implementation of SecurityManager and a custom ClassLoader called SandboxLoader) and a service class which allows to access the functionality. A sandbox can be initiated only using the security manager or using the classloader together with the security manager. We call the refer to the first possibility (security manager only) as simple sandbox and to the second way as regular sandbox or simply sandbox. We will briefly describe the difference between the two modes. For further information on how the java sandbox works internally have a look at the examples and explanations further down.
Assume service is an instance of the java-sandbox's service class. Then the easiest way to create a simple sandbox is to call
String pw = service.restrict(context); try{ /* put untrusted code here */ } finally { service.releaseRestriction(pw); }
With this setup only the security manager is enabled to supervise the execution of the untrusted code. The security manager is asked before certain system resources can be used or, for example, whether or not the System.exit() may be called (more information on the capabilities of security managers and the permission concept can be found here, and here). The context object of type SandboxContext allows to configure which permissions are given.
One problem with the above setup is that it is hardly possible to restrict access to classes and packages. Although security managers have a concept of being asked whether or not classes from certain packages may be loaded the security manager is asked exactly once by the classloader when loading the class. Thus, if a package has been accessed outside the sandbox it is also cleared for use within the sandbox (at least in the eyes of the corresponding classloader).
To solve this problem we load the untrusted code with a custom classloader. The java-sandbox provides several easily accessible methods to do this. The most common is probably to run the untrusted code within a SandboxedEnvironment object (which is similar to a standard Callable object).
SandboxedEnvironment<Object> c = new SandboxedEnvironment<Object>() { @Override public Object execute() { /* untrusted code */ return null; } }; service.runSandboxed(c, context);
The runSandboxed methods accept a SandboxedEnvironment and a SandboxContext as input. The context defines the permissions and configuration of the sandbox. To securely execute the code wrapped in the environment the environment class is reloaded with a custom classloader. It then creates a sandbox and calls the environment's execute method. By loading the environment in a custom classloader we have full control over which classes are loaded. The custom classloader also not only informs the security manager on packages but on classes that are to be loaded thus allowing to fine tune the sandbox to the specific needs.
In the following we give several examples of how to use the java-sandbox. For further information also see the [API-specification] and tutorials.
The first step is to enable the sandbox service and install the security manager. For this it is sufficient to instantiate the SandboxService
SandboxService service = new SandboxServiceImpl();
located in package net.datenwerke.sandbox. This will install the security manager with default parameters. Note that the service should be used as a singleton. You can also access the service by calling its static method getInstance(). By default the sandbox service starts two so called remote agents, that is two extra java virtual machines that can be used for remote sandboxing. To not create these processes either provide the necessary parameters to the constructor or use the static initialization method
SandboxServiceImpl.initLocalSandboxService();
The SandboxService object provides access to all necessary functionality. To create a simple sandbox (that is one which uses only the security manager and not a custom class loader) it provides methods restrict() and releaseRestriction(String). Restriction generates (or takes) a password, which is needed to, again, deactivate the sandbox. To create proper sandboxes the SandboxService provides the methods runSandboxed() methods. To only use the SandboxLoader (the sandbox's class loader) but not to directly invoke the sandbox use the methods runInContext().
The entire configuration of a sandbox is given by a SandboxContext object. For the configuration we distinguish several parts:
We will discuss these in turn. Some advanced options are later touched upon in the examples. Also have a look at the [API of SandboxContext.]
Class-level Permissions
Via class-level permissions you can control which classes can be accessed from within the sandbox. Note that these checks are only performed if the sandbox is run with the SandboxLoader class loader, which is, for example, the case if you construct the sandbox via the runSandboxed() commands.
You can either allow access to classes or deny class access. Whenever checks are performed it is first evaluated if a specific permission is specifically denied before it is checked whether any of the rules might grant access. Thus, it is possible to allow access to all classes in java.lang while, for example, denying access to java.lang.System. To configure class-level access use the following methods:
To allow access to any class you can set to bypass class access checks by setting setBypassClassAccessChecks(). The default is to enable class level checking.
Permission objects such as ClassPermission or SecurityPermission can be configured to consider the current stack trace. Thus, it is possible to define permissions such as: class foo may be accessed if the caller stack at position 4 is class bar. For this, the permission objects take a StackEntry object which is instantiated as
new StackEntry(int pos, String type, boolean prefix)
where pos is a position in the stack trace (set this to -1 to denote any position in the stack trace), type is the class and prefix denotes whether it is sufficient that the class in the stack trace is a prefix of the defined type or if they have to match exactly. If multiple StackEntries are passed to a permission object than the permission is granted if and only if all StackEntries validate.
Package-level restrictions are defined analogously to class-level restrictions. Note however, that we do not recommend to use package-level.
To allow access to any package you can set to bypass class access checks by setting setBypassPackageAccessChecks(). The default is to disable package level checking (we advocate the use of class level checks).
Filesystem permissions are configured using the FilePermission interface. There are 4 default implementations: FileEqualsPermissions, FilePrefixPermission, FileRegexPermission and FileSuffixPermission which are given a name and test if a given file(name) is equal, has the same prefix or suffix or whether it matches the regular expression. Use the addFilePermission method of SandboxContext to add a permission to the whitelist (AccessType.PERMIT) or blacklist (AccessType.DENY).
If class access is restricted, the class loader should be given read access for objects on the classpath (otherwise it cannot load any class). For this you can use the convenience method addClasspath(). Further convenience methods are
Via general security permissions you can blacklist or whitelist requests for java.security.Permissions. These are made by (trusted) code whenever critical resources are requested (for example, the method getClassLoader on a Class object will ask the security manager if the "java.lang.RuntimePermission" "createClassLoader" is granted. You can find an introduction here. To grant (or deny) permissions use the addSecurtiyPermission() method in SandboxContext.
To grant access for any permission (this includes FilePermissions) you can set setBypassPermissionAccessChecks(). The default is to enable permission checking.
As we explained above it is crucial that the untrusted code is loaded in a special class loader, this loader being the [SandboxLoader]. From here on, any classes are loaded by the SandboxLoader (with the exception of classes in java.). In some instances you might, however, need to load classes with your regular class loader. To tell the SandboxLoader not to load certain classes directly, but rather use its parent class loader use the methods
To override behavior defined with the above methods you can use the addClassForSandboxLoader(String, Mode) method.
Besides telling the SandboxLoader which class loader to use for loading classes you have the following options:
When generating proper sandboxes (that is using a custom classloader) via the SandboxService's methods [runSandboxed] and [runInContext] you can specify the following options.
setRunInThread()
Wraps the execution of the sandboxed code in an own thread. This thread is, furthermore, monitored and you can specify a maximum runtime and a maximum stack-depth using
setMaximumRunTime(long)
setMaximumStackDepth(int)
setRunRemote()
Tells the SandboxService to execute the sandboxed code on a separate java virtual machine. See treatment of remote sandboxes below.
To debug you can set the SandboxContext into debug mode using the setDebug() method. Java-sandbox uses the standard java.util.logging framework to output debug information.
Before you can use any sandbox you need to enable the sandbox service. For this it is sufficient to simply load an instance of SandboxServiceImpl. Note that this service should be used as a singleton.
SandboxService sandboxService = SandboxServiceImpl.getInstance();
This will install the security manager and from now on, you can use the sandbox services. If you are not planning to use remote agents (see documentation of remote sandboxes below) you should initialize the sandbox service using
SandboxServiceImpl.initLocalSandboxService();
Note that this is equivalent to
new SandboxServiceImpl(true, new SandboxCleanupServiceImpl(), null);
To invoke a simple sandbox without a custom class loader (note that in this case the sandbox will only track security permissions see above) it is sufficient to create a context and call the restrict method on the SandboxService object:
SandboxContext context = new SandboxContext(); String password = null; try { password = sandboxService.restrict(context); /* untrusted code goes here */ } finally { sandboxService.releaseRestriction(password); }
To invoke a proper sandbox using the SandboxLoader as class loader we need to wrap the untrusted code into an object, such that it can be loaded by the SandboxLoader. For this the sandbox service class offers convenience methods that allow you to pass a SandboxedEnvironment object (which is essentially something like a Callable object) which is loaded into the SandboxClassloader and then executed.
SandboxedEnvironment<Object> c = new SandboxedEnvironment<Object>() { @Override public Object execute() throws Exception { /* run untrusted code */ /* return some value */ return null; } }; SandboxContext context = new SandboxContext(); SandboxedCallResult<Object> result = sandboxService.runSandboxed(c.getClass(), context);
As you can see the runSandboxed command returns an object of type [SandboxedCallResult] which is a wrapper around the object returned by the environment. Note that the SandboxedEnvironment runs inside the SandboxLoader while the calling code usually is loaded in a different class loader. Thus, the result of the computation is necessarily also in the scope of the SandboxLoader and not of the outside application class loader. The SandboxCallResult object allows you to bridge between the two class loaders. That is, it provides the following methods:
Let us note that the cloning of objects from one classloader to the other can fail for complicated object graphs. You should thus try to ensure that the return value is of a rather simple type.
Often you might want to pass values into the SandboxedEnvironment. There are several ways to do this, which we will explain now. One option is that the code from within the SandboxedEnvironment accesses member variables from the outer class. This is the case in this example:
public class ExampleClass { public String myValue = "This is some value"; public void run(){ SandboxService sandboxService = SandboxServiceImpl.getInstance(); SandboxedEnvironment<String> c = new SandboxedEnvironment<String>() { @Override public String execute() throws Exception { /* run untrusted code */ System.out.println(myValue); /* return some value */ return "This is a different value"; } }; /* configure context */ SandboxContext context = new SandboxContext(); context.addClassForApplicationLoader(getClass().getName()); context.addClassPermission(AccessType.PERMIT, "java.lang.System"); context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream"); /* run code in sandbox */ SandboxedCallResult<String> result = sandboxService.runSandboxed(c.getClass(), context, this); /* output result */ System.out.println(result.get()); } }
Let us go through the example step by step. We have a class ExampleClass which executes the sandboxed code within its run method. We call the classloader which loaded ExampleClass the application class loader. The example looks pretty similar to the previous example except that the SandboxedEnvironment object accesses the member variable myValue from the surrounding ExampleClass instance. To understand what happens, you need to understand how java internally compiles anonymous classes that access outside variables. For this java generates a constructor for the SandboxedEnvironment object which expects a single parameter of type ExampleClass. To ensure that this parameter is set, when the SandboxedEnvironment object is loaded and instantiated by the sandbox service we need to supply the sandbox service with the necessary object. This is done in the runSandboxed call. The last parameters (if specified) are passed to the constructor.
One problem, however, is that the SandboxedEnvironment attempts to load the class ExampleClass and as itself is loaded in the SandboxLoader it asks the SandboxLoader to load it. When we pass the outer instance to the constructor the outer instance was loaded using the application loader. As two classes are only equivalent if they are loaded by the very same classloader java would complain here, saying something like
argument type mismatch: Could not instantiate the Callable obect. Is there a classloader problem? The Callable's constructor expects post.Example$1(post.Example) but I got: [post.Example@3dee2310, ]
To avoid this, we need to make sure, that ExampleClass is always loaded by the application loader. For this, we configure our SandboxContext to load the ExampleClass using the application loader:
context.addClassForApplicationLoader(getClass().getName());
As we access System.out.println from within the sandbox we also need to allow class access to java.lang.System as well as java.io.PrintStream.
Note that it is not possible to access non-public member variables via this method.
A second, more flexible, and usually preferred approach is to use a custom class for the environment. That is, not an anonymous class, but a defined class with a defined constructor. Assume we have the following SandboxedEnvironment class:
public class MyEnvironment implements SandboxedEnvironment<String> { private final String myValue; public MyEnvironment(String myValue){ this.myValue = myValue; } @Override public String execute() throws Exception { /* run untrusted code */ System.out.println(myValue); /* return some value */ return "This is a different value"; } }
Then we invoke sandbox the sandbox using the following code:
SandboxService sandboxService = SandboxServiceImpl.initLocalSandboxService(); /* configure context */ SandboxContext context = new SandboxContext(); context.addClassPermission(AccessType.PERMIT, "java.lang.System"); context.addClassPermission(AccessType.PERMIT, "java.io.PrintStream"); /* run code in sandbox */ SandboxedCallResult<String> result = sandboxService.runSandboxed(MyEnvironment.class, context, "This is some value"); /* output result */ System.out.println(result.get());
To properly protect against malicious code we want to run the sandbox in an extra thread. This allows us to better monitor what the untrusted code is doing. To enable that a custom thread is generated in which the SandboxedEnvironment is executed simply set the runInThread() method on the SandboxContext object.
Assume we have a malicious environment such as the following:
public class InfiniteLoopEnvironment implements SandboxedEnvironment<Object> { @Override public Object execute() throws Exception { while(true){ try{ Thread.sleep(1000); } catch(InterruptedException e){} } } }
Calling the execute method would block the calling thread indefinitely. Using the option to run this in a threaded sandbox allows us to kill the thread if necessary. Consider the following calling code.
SandboxService sandboxService = SandboxServiceImpl.initLocalSandboxService(); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); context.setMaximumRunTime(2, TimeUnit.SECONDS, RuntimeMode.ABSOLUTE_TIME); /* run code in sandbox */ SandboxedCallResult<String> result = sandboxService.runInContext(InfiniteLoopEnvironment.class, context);
We have configured the sandbox to run in a thread and that the maximum runtime should not exceed 2 seconds. The runInContext method works exactly as the runSandboxed method except that the securitymanager is not immediately activated (this could for example be part of the environments task). If we run the above code we get the following exception
Exception in thread "main" net.datenwerke.sandbox.exception.SandboxedTaskKilledException: killed task as maxmimum runtime was exceeded at net.datenwerke.sandbox.SandboxMonitorDaemon.testRuntime(SandboxMonitorDaemon.java:82) at net.datenwerke.sandbox.SandboxMonitorDaemon.run(SandboxMonitorDaemon.java:57)
at java.lang.Thread.run(Thread.java:722)
When reading this, you should ask the question: how does the java-sandbox library kill the wild running thread. For this we use the deprecated Thread.stop(). This method can potentially be problematic which is why it was deprecated in the first place. In a nutshell the problem is that when the thread is killed any locks held by the thread are immediately released. This in turn can lead to objects being corrupted (if for example the killed thread was not yet finished initializing an object that is shared between multiple threads). However, sandboxed code will in many cases not be allowed to access shared objects. In these instances it should be safe to stop threads the hard way. To provide more control about what is going on, the sandbox service analyzes the thread on stopping and informs you if the thread held any locks. If this is not the case we assume it safe to kill the thread.
Consider the following adaption of the above environment.
public class InfiniteLoopEnvironment implements SandboxedEnvironment<Object> { @Override public Object execute() throws Exception { synchronized (this) { while(true){ try{ Thread.sleep(1000); } catch(InterruptedException e){} } } } }
Now if run the following calling code:
SandboxService sandboxService = SandboxServiceImpl.initLocalSandboxService(); sandboxService.attachHandler(new BadThreadKillHandler() { @Override public void badThreadKilled(BadKillInfo killInfo) { System.out.println("bad thread was killed"); } }); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); context.setMaximumRunTime(2, TimeUnit.SECONDS, RuntimeMode.ABSOLUTE_TIME); /* run code in sandbox */ SandboxedCallResult <string>result = sandboxService.runInContext(InfiniteLoopEnvironment.class, context);</string>
we are informed about that a thread still holding locks was stopped and might take action.
A possible way to deal with this issue is to run sandboxed code in an extra process and not just in an extra thread.
By default the sandboxing service starts two extra java virtual machines. One which is used as a so called freelancer (we get to this) and the other to execute sandboxed code transparently. To run a SandboxEnvironment on a remote agent, simply configure the SandboxContext using the setRunRemote method. Consider the following environment:
public class RemoteEnvironment implements SandboxedEnvironment <string>{ @Override public String execute() throws Exception { return ManagementFactory.getRuntimeMXBean().getName(); } }</string>
That is, the environment simply outputs the process name. Let us first run this code in its own thread but on the same machine:
SandboxService sandboxService = SandboxServiceImpl.getInstance(); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); /* run code in sandbox */ SandboxedCallResult <string>result = sandboxService.runInContext(RemoteEnvironment.class, context); System.out.println(ManagementFactory.getRuntimeMXBean().getName()); System.out.println(result.get());</string>
On my machine the output would look like
Jun 12, 2013 7:33:21 PM net.datenwerke.sandbox.jvm.server.SandboxJvmServer <init>
INFO: started sandbox server: SandboxRemoteServerNr1
Jun 12, 2013 7:33:22 PM net.datenwerke.sandbox.jvm.server.SandboxJvmServer <init>
INFO: started sandbox server: SandboxRemoteServerNr2
30020@AMBPro.local
30020@AMBPro.local
The first two info messages tell me that two remote agents were initialized. But the process id of the calling code and that of the sandbox are the same. If we now add the following line to the context definition:
context.setRunRemote(true);
The output changes to
31134@AMBPro.local
31136@AMBPro.local
indicating that the sandbox was actually executed on a different process.
The communication between the main java process and the remote agent is implemented using RMI. Thus, we can easily pass parameters into our SandboxEnvironment as before, with the only exception that the parameters need to implement the Serializable interface.
Consider the following environment:
public class RemoteEnvironment implements SandboxedEnvironment<String> { private String value; public RemoteEnvironment(String value){ this.value = value; } @Override public String execute() throws Exception { return "I have been given value: " + value; } }
If we called this using the following setup
SandboxService sandboxService = SandboxServiceImpl.getInstance(); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); context.setRunRemote(true); /* run code in sandbox */ SandboxedCallResult <string>result = sandboxService.runInContext(RemoteEnvironment.class, context, "Let's pass a value"); System.out.println(result.get());</string>
we would get the following output
Jun 12, 2013 7:50:20 PM net.datenwerke.sandbox.jvm.server.SandboxJvmServer <init>
INFO: started sandbox server: SandboxRemoteServerNr1
Jun 12, 2013 7:50:21 PM net.datenwerke.sandbox.jvm.server.SandboxJvmServer <init>
INFO: started sandbox server: SandboxRemoteServerNr2
I have been given value: Let's pass a value
When invoking many small remote sandboxes the performance bottleneck is the creation of the class loader on the remote agent. To cache this class loader you can use what we coined [JvmFreelancers]. Lets say we have a SandboxContext and want to reuse this object for many invocations. If we changed the above calling code to
SandboxService sandboxService = SandboxServiceImpl.getInstance(); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); context.setRunRemote(true); /* run code in sandbox */ long l = System.currentTimeMillis(); for(int i = 0; i < 1000; i++) sandboxService.runInContext(RemoteEnvironment.class, context, "Let's pass a value"); System.out.println("Time: " + (System.currentTimeMillis() - l));
I'll get an output like "Time: 3218" on my machine. If we use a freelancer, this can be drastically optimized:
SandboxService sandboxService = SandboxServiceImpl.getInstance(); /* configure context */ SandboxContext context = new SandboxContext(); context.setRunInThread(true); context.setRunRemote(true); context.addClassForApplicationLoader("sun.reflect."); long l = System.currentTimeMillis(); JvmFreelancer freelancer = sandboxService.acquireFreelancer(); freelancer.init(context); try{ for(int i = 0; i < 1000; i++){ freelancer.runInContext(RemoteEnvironment.class, "pass over some value"); } } finally { sandboxService.releaseFreelancer(freelancer); } System.out.println("Time: " + (System.currentTimeMillis() - l));
Now I get a run-time of 1092ms. If the sandbox needs to load more classes the speedup increases even more.
Note two things about the above code. First, the freelancer should be released to the sandboxService, after it isn't needed anymore. Seconde, we added
context.addClassForApplicationLoader("sun.reflect.");
to tell the SandboxLoader to load any class within the sun.reflect. packages with the system class loader. I haven't really figured out when reflection makes use of classes in sun.reflect, but for some reason after the 15th iteration of the above loop it does and complains badly that it is not loaded from the system class loader.
Reflection is the enemy of the sandbox. If a user were able to get his or her hands on, for example, the SandboxSecurityManager then it could easily escape the sandbox. Consider the following code snippet:
Object manager = System.getSecurityManager(); Class clazz = manager.getClass(); Field f = clazz.getDeclaredField("restrict"); f.setAccessible(true); Object tl = f.get(manager); Class tlClazz = tl.getClass(); Method setMethod = tlClazz.getMethod("set", Object.class); setMethod.invoke(tl,new Object[]{ null }); System.out.println("I've broken out!\n");
If we run this code inside a sandbox the only two permissions that are checked are:
PermissionCheck: ("java.lang.RuntimePermission" "accessDeclaredMembers")
PermissionCheck: ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
The problem is, that to not permit reflection is often not possible. Take groovy as example. Groovy handles most of its logic via reflection. So how can we protect against this attack? We could try to deny access to the System class. But that might also not work since some code might need it. We can also not deny access to the SandboxSecurityManager class, because it is not even loaded in this context.
The [reflective sandcastle] is a simple extension to the permission checks done by java when encountering reflection. It allows for permission checking at class level and thus, for example, to allow reflection in general, but not for classes SandboxLoader and SandboxSecurityManager. In fact, the SandboxContext is by default configured such that these two classes cannot be accessed via reflection (if the reflective sandcastle is enabled).
To enable the reflective sandcastle simply download the library and add it to the boot classpath:
-Xbootclasspath/p:/PATH-TO/reflective-sandcastle-java7-0.1.jar
You will find more information in the [documention of the reflective sandcastle].