Some Java applications need to be able to load plugins, in the form of .jar files, from untrusted sources, and execute code from such plugins with restricted permissions, for example such that the code is not able to read or write arbitrary files (or any files at all), or not able to establish network connections. However, code which is considered part of the main application should not be restricted.
What we want to do, is implement a sandbox, such that
- We can execute application code alongside plugin code in the same JVM
- The application can load plugins and execute their code on demand, ie. without plugin .jar-files being on the applications classpath
- Plugin code can execute with restricted permissions
- Application code can execute “outside” the sandbox, ie. without being restricted
A simple sandbox implementation
The Java security policy provides a way of defining different sets of permissions for application code and plugin code. We extend java.security.Policy
to achieve this:
public class SandboxSecurityPolicy extends Policy { @Override public PermissionCollection getPermissions(ProtectionDomain domain) { if (isPlugin(domain)) { return pluginPermissions(); } else { return applicationPermissions(); } } private boolean isPlugin(ProtectionDomain domain) { return domain.getClassLoader() instanceof PluginClassLoader; } private PermissionCollection pluginPermissions() { Permissions permissions = new Permissions(); // No permissions return permissions; } private PermissionCollection applicationPermissions() { Permissions permissions = new Permissions(); permissions.add(new AllPermission()); return permissions; } }
Our policy implementation assigns different sets of permissions to different ProtectionDomain
s. A protection domain correponds to a set of classes which should have the same permissions, so for different protection domains we can have different permissions. We determine if a protection domain corresponds to plugin code by examining the type of the class loader for the protection domain, and return one fixed set of permissions for plugins (pluginPermissions()
) and another fixed set of permissions for application code (applicationPermissions()
).
The PluginClassLoader is the class loader to be used for plugins:
public class PluginClassLoader extends URLClassLoader { public PluginClassLoader(URL jarFileUrl) { super(new URL[] {jarFileUrl}); } }
It doesn’t do a lot except adding a convenience constructor, but it allows the policy to distinguish the plugin class loader with the instanceof
check in SandboxSecurityPolicy.isPlugin()
.
Finally, to make this work, we must set the security policy and the security manager during application initialization:
Policy.setPolicy(new SandboxSecurityPolicy()); System.setSecurityManager(new SecurityManager());
Note, that, by default, there is no security manager, and unless we set one, the policy will have no effect, and the sandbox will not be active. Also note, that the policy must be set before setting the security manager, otherwise the default policy, which is in effect immediately after setting the security manager, will not allow the application to set the policy.
Now, to load a plugin, do:
ClassLoader pluginLoader = new PluginClassLoader(pluginJarFile); Class<?> pluginClass = pluginLoader.loadClass("foo.bar.PluginImplementation"); Plugin plugin = (Plugin) pluginClass.newInstance();
Here, Plugin
is typically an interface defined by the application, while foo.bar.PluginImplementation
defined by the plugin implements that interface.
With this in place, application code can run unrestricted, while plugins will be denied access to the fun:
application.readSensitiveFilesAndPhoneHome(); // OK plugin.readSensitiveFilesAndPhoneHome(); // Will throw java.lang.SecurityException
Permissions
Permissions need not be all-or-nothing. Sometimes it makes sense to allow plugins some permissions, while denying other permissions.
The Permission
class and its subclasses represent different things you can have permission for. For example, new FilePermission("/foo/bar", "read")
allows you to read file /foo/bar, while new SocketPermission("localhost:8081", "connect")
allows you to connect to localhost on port 8081. Have a look at the permission class hierarchy to see what is possible.
Two permission classes deserve special mention: AllPermission
is a pseudo-permission implying all other permissions. RuntimePermission
covers a slew of different system level permissions such as the ability to read environment variables, exit the JVM, or change the security policy.
For example to allow plugins to read and write files, but only in a particular directory we could change our SandboxSecurityPolicy
like this:
private PermissionCollection pluginPermissions() { Permissions permissions = new Permissions(); permissions.add(new FilePermission("/my-application/plugin-workspace/*", "read,write")); return permissions; }
Protection domains, policies and class loaders
A ProtectionDomain
corresponds to a set of classes, for which the same permissions should apply. It is characterized by the source of the classes (eg. a particular jar file) and the class loader used for loading the classes.
The Policy
allows you specify permissions for protection domains. The policy can determine the permission by examining not only the class loader for the protection domain, but also the code source (eg. jar file url) and any code signing information for the code.
In addition to the permissions defined by the policy, the class loader also defines a set of permissions which can be controlled by overriding SecureClassLoader.getPermissions()
. This set of permissions is assigned to the ProtectionDomain
for the class loader and code source, and is combined with the permissions given by the policy.
So for example to give access to some file for all code loaded by our class loader, regardless of the Policy, do:
public class PluginClassLoader extends URLClassLoader { // ... @Override protected PermissionCollection getPermissions(CodeSource codesource) { Permissions permissions = new Permissions(); permissions.add(new FilePermission("/some/file", "read")); return permissions; } // ... }
How the JRE checks permissions at run time
The java.security.AccessController
checks if permissions are granted at run-time. It works like this: When checking if some permission should be granted, the AccessController
examines the current call stack, and determines all protection domains involved. If all relevant protection domains have the permission, the permission is granted.
For our sandbox, this means that if untrusted plugin code calls trusted code to make it do something sinister on its behalf, the permission will be denied because untrusted plugin code is on the call stack.
The SecurityManager
is mostly a leftover from older versions of java. The built in SecurityManager
implementation simply delegates to the AccessController
for checking permissions. However, a SecurityManager
must be set with a call to System.setSecurityManager()
, otherwise checking of permissions is completely disabled.
Caveat: The explanation given here only gives an overview of the java security classes and addresses the aspects supporting our sandbox implementation. The classes have many other uses and a lot more complexity than what we can cover here. See Java SE reference documentation.
CPU and resource hogging
Sandboxing does not prevent plugins from hogging the CPU, from never returning control to their caller or from using up all heap space. To effectively guard against these kinds of bad behaviour you may have to run plugins in their own JVM.
Inspecting and subverting your application state
When passing data as arguments to the plugin, sandboxing does not prevent the the plugin from inspecting or modifying data in unforeseen ways.
Consider an example: An application processes documents of some kind which are stored on disk, and the application takes care of reading and writing documents to disk files. If a document has been read from file, the document “remembers” its file location, to allow the user to easily save the document back to the same file, so the Document
class has a fileName
property. The application provides a document as an argument to the plugin with the intention that the document can read and update the document. However, the plugin can also set the fileName
property, and can therefore trick the application into overwriting arbitrary files the next time the application saves the document.
To guard against this, examine the object models of any data you pass to the plugin to ensure that plugins cannot modify the data in unexpected ways and that you do not unintentionally reveal sensitive information. Also consider defensively copying any data passed to the plugin, or pass only immutable data.
Be aware, that untrusted code may also use reflection to inspect or modify data. The relevant permissions controlling this are: RuntimePermission("accessDeclaredMembers")
, which allows access to declared members of a class using reflection, and ReflectPermission("suppressAccessChecks")
which allows bypassing the access checks for reflective access to members with private, protected or default access.
Summary
Extending the security policy to programmatically assign different permissions to application code and untrusted plugin code seems to be the simplest way of implementing a sandbox. The Java security api, however, provides other possibilities which you might want to explore. It also allows you to make more fine grained control over the sandbox behavior, for example to implement multiple levels of protection or to take code signing into account.
Brilliantly concise. Thanks for this. There’s not nearly enough example code out there to make sense of the rather complicated Java security model.
I detected a small issue with the example, while technically everything you articulated is correct, there is some nuance that might be helpful to others.
In order to make the example actually work i.e. to instantiate a class and do things like:
application.readSensitiveFilesAndPhoneHome(); // OK
plugin.readSensitiveFilesAndPhoneHome(); // Will throw java.lang.SecurityException
… you must normally have the plugin jar file on the classpath. But if a class is on the classpath – then the custom loader you built will default to the regular system/primordial classloader, as it does for all classes on the classpath. Once this happens, the custom classloader you have written has basically no effect and the security scheme has no effect either.
So – in order for the custom classloader you built to actually load the class and not delegate to the system loader, you have to remove any plugin jar files from the classpath. Once you do this, the line of code:
Plugin plugin = (Plugin) pluginClass.newInstance();
Must imply that the class ‘Plugin’ is some kind of generic/abstract class, and that the real plugin must effectively inherit from this because of course, you cannot reference the real plugin implementation in your code without putting it on the classpath – which because of the above reasons – you cannot do!
It’s a fine point, but the notion that the custom class loader has no effect if the underlying third party jar files are on the classpath I think is worth noting.
Alternatively, you could adapt your custom classloader to *not* delegate to the default classloader given a set of rules, in which case it does not delegate. But this would be some work as you’d likely have to find and load the bytes yourself, manually.
FYU – the system classloader is written in native code and most of the useful methods are private in the class ClassLoader, it’s difficult to simply ‘override’ and make use of them.
Forgive me if I am mistaken.
Thanks for the comment.
If a jar file is already on the classpath of the application i would not consider it a plugin. The premise here is that plugin code is less trustworthy than application code – so a plugin jar file should not be allowed on the classpath.
By the way, it is not strictly necessary to have your own custom class loader class, it is just convenient, as it allows the policy easily to distinguish plugin protection domains. But the policy could do this in other ways too, for example by examining ProtectionDomain.getCodeSource().
In the example, Plugin is meant to be an interface defined by the application. A plugin jar file provides an implementation of this interface. I updated the post to reflect this.
Pingback: itemprop="name">Java SecurityManager - Tomcat - Use custom security policy - CSS PHP
Hey thanks for this great article. This is the first one that makes sandboxing more clear to me!
When trying out your approach I ran into a problem. My application starts with the main-method and I want so set the Policy and the SecurityManager programmatically. The policy implementation is called for the ExtClassloader which is granted all permissions. The AppClassloader’s permissions are not updated by the policy implementation. The AccessController simply does not call getPermissions for the AppClassloader. Note that those classloaders are the default java classloaders. This problem has nothing to do with my custom plugin classloaders.
The effect of this problem is, that if I start using a SecurityManager the whole application is restricted. For example I cannot redirect System.in/out/err because this permissions is not granted – but I have AllPermissions granted in the Policy implementation. How can this be?
Hi Chris,
Are you sure your custom policy as well as a security manager are properly setup? You might want to use a debugger on the AccessController to check why getPermissions() for your custom policy is not called.
Java brings some of the most fascinating features or benefits that are impossible to find in any other programming languages or platforms. Many thanks for sharing this.
Pingback: itemprop="name">Java Applications with Plugins – Security – Learn Something Quick
Pingback: itemprop="name">How to run a kotlin jar in a sandbox? - JavaTechji
Pingback: itemprop="name">Java Sandboxing and ProcessBuilder - JavaTechji