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!
19 Comments:
Nice blog, it will help for sure.
Thanks :)
I'd like to try out your bot. Which network and channel can I find it on?
I tend not to run it persistently, it's still very much under development. If you like I can drop it into a channel for a while, though.
That's a very useful technique. However, in general untrusted bytecode can escape. Bytecode really needs an appropriate protection domain set when loaded. Interpreters are more difficult.
Interesting, Tom! Part of the reason I posted this article was to see if my approach had flaws that people could enlighten me to, since I'm really not completely familiar with the intricacies of the Java security model. Could you elaborate a little on the specific problems here, I'm very interested :)
For what it's worth there are problems with this approach even the way I'm using it; most of the interpreters (unless you instantiate them more directly, i.e. not through the javax.script mechanism) require the createClassLoader RuntimePermission, which is a pretty risky one in general; it's not great.
Calum: How about #scala on freenode? I'm 'mapreduce' there, if you want to mention it to me when it's there.
Alrighty Ricky, I'll look into whacking it up there at some point, tomorrow is the earliest likely time though!
"However, in general untrusted bytecode can escape"
Hmmm...what is being implied here? That untrusted bytecode can escape or 'take over' the VM and execute arbitrary code?
I think that might've been the implication, jeff. It might've bytecode derived from non-Java sources, of course.
So...are there KNOWN unpatched security vulnerabilities in the sun JVM (on platform xyz) that can allow 'escape' of untusted code?
Not as far as I'm aware, but remember the code I'm sanboxing assumes that null is an "unknown" domain — there's any number of reasons why null might be trusted by default. It seem silly to me, but still.
jeff, the correct way to load bytecode that is no to be trusted is to give it an untrusted protecton domain when it is loaded. So, untrusted bytecode getting around the technique presented in this weblog entry is clearly not a vulnerability.
As an example of how code might escape, finalizers will be execute ina finalizer thread. This is a different thread and hence the doPivileged will not be on the stack. The permissions in effect will just be the intersection of the protection domains on the finalizer stack. So if the protection domain is not set for the untrusted bytecode, you will in effect be trusting it.
I believe this is quite true, yes. I'm not convinced that the "null" protection domain is considered trusted, however, I think those finalizers may be run with no permissions at all. Which is quite possibly not desired in itself.
This was VERY helpful, thank you.
Im also working on an app that has to run sandboxed script code and Id prefer it be in an arbitrary script engine.
I've been beating my head against the problem for a few days, but learned a few things here I didnt realize before that should help alot!
CyberQat: I'm glad this was useful to you, but please bear in mind that you cannot use the SecurityManager API to stop people spawning threads (I don't know why this option isn't there but that's the case). If someone keeps spawning threads it will result in something like a DOS attack and can slow the machine down, although eventually it'll all collapse with an OutOfMemoryError (this is thrown when you run out of threads, not sure why it's not got its own exception).
In general, in any case, be weary of people spawning threads.
Actually, to append to this, I believe you can use ThreadGroups to impede thread creation. I just read the docs wrongly when I looked into them. Oh well!
Hey, I'm trying to do almost the same thing for Clojure. It sounds like this almost might be a good opensource project, a nice canned sandbox, insert your interpreter here. If you decide to do this I'm annie66us at that yodeling company
It seems like so little work to construct that I'm almost loathe to make it a project, if I'm honest Annie. The other issue I found was that different language interpreters require different "basic" sets of permissions.
More than anything though, my hands re: open source hackery are pretty tied by my employer. There's a "process", you see.
Post a Comment
Subscribe to Post Comments [Atom]
<< Home