Self-Protecting Sandbox using SecurityManager
TL:DR; Most sandbox mechanisms involving Java's SecurityManager do not contain mechanisms to prevent the SecurityManager itself from being disabled, and are therefore "defenseless" against malicious code. Use a SecurityManager and a security policy as a system property on startup to cover the entire JVM, or use an "orthodox" sandbox as described below.
Background
Since looking at the Java Serialization vulnerability, I've been thinking about mitigations and solutions in the JVM. I started with a Java agent, notsoserial, as a way to disable Java serialization itself. But that just opened up a larger question – why should the JVM let you call runtime.exec
to begin with? Poking into that question led me to Pro-Grade and looking at blacklists of security manager permissions as a solution.
The problem with blacklists is that there's always a way to work around them. Simply disabling the "execute" file permission in the JVM didn't mean anything by itself – what's to say that it couldn't simply be re-enabled? What prevents malicious code from turning off the SecurityManager?
I thought it was possible, but I didn't know exactly how it could happen. I've never worked on a project that worked with the SecurityManager, or added custom security manager checks. Most of the time, the Java security manager is completely disabled on server side applications, or the application is given AllPermission, essentially giving it root access to the JVM.
Broken Sandboxes
All my suspicions were confirmed in a recent paper from a team at CMU, Evaluating the Flexibility of the Java Sandbox:
[D]evelopers regularly misunderstand or misuse Java security mechanisms, that benign programs do not use all of the vast flexibility afforded by the Java security model, and that there are clear differences between the ways benign and exploit programs interact with the security manager.
The team found that most of the policies that programmers put in place to "sandbox" code so that it did not have permissions to do things like execute files were not effective. In particular, they call out some security manager idioms as "defenseless" – they cannot prevent themselves from being sidestepped. In a nutshell, while sandboxed applications may not execute a script on the filesystem, they can still modify or disable the security manager.
The team looked through 36 applications that used the SecurityManager, to see how application programmers work with the security architecture.
Every single one of them failed.
All of these applications ran afoul of the Java sandbox's flexibility even though they attempted to use it for its intended purpose. […] While Java does provide the building blocks for constraining a subset of an application with a policy that is stricter than what is imposed on the rest of the application, it is clear that it is too easy to get this wrong: we've seen no case where this goal was achieved in a way that is known to be free of vulnerabilities.
I think there are times when the disconnect between security professionals and application developers turns into a gulf. At the same time that the Oracle Secure Coding Guide has a section on access control, and the CERT guide talks about protecting sensitive operations with security manager checks, there's very little about how to set up a secure environment that can use SecurityManager effectively at all.
Sandbox Defeating Permissions
Here are the permissions that make up a defenseless sandbox:
RuntimePermission("createClassLoader")
RuntimePermission("accessClassInPackage.sun")
RuntimePermission("setSecurityManager")
ReflectPermission("suppressAccessChecks")
FilePermission("<<ALL FILES>>", "write, execute")
SecurityPermission("setPolicy")
SecurityPermission("setProperty.package.access")
Given any one of these, sandboxed code can break out of the sandbox and work its way down the stack. The AllPermission
in particular allows all of the above, and the team specifically calls out blacklists as ineffective:
[R]estricting just one permission but allowing all others results in a defenseless sandbox.
The prograde-example I showed earlier only restricts one permission, and allows all others. It's absolutely better than nothing, but it can be worked around.
deny {
permission java.io.FilePermission "<<ALL FILES>>", "write, execute";
permission java.lang.RuntimePermission "createClassLoader";
permission java.lang.RuntimePermission "accessClassInPackage.sun";
permission java.lang.RuntimePermission "setSecurityManager";
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";
permission java.security.SecurityPermission "setPolicy";
permission java.security.SecurityPermission "setProperty.package.access";
};
In addition, if you're using pro-grade as a blacklist, you'll need to disable the charset provider – otherwise you're essentially allowing mutable strings;
// http://slightlyrandombrokenthoughts.blogspot.com/2009/07/java-se-security-part-ii-immutability.html
deny {
permission java.lang.RuntimePermission "charsetProvider";
}
Some of these permissions are ubiquitious, such as the access to the entire 'sun' package. Still, if you have an application server, you should look at the policy file and see if any of the above permissions are explicitly granted.
The Orthodox Sandbox
The team discusses an effective sandbox techniques in passing:
[…] Java provides an “orthodox” mechanism to achieve this goal while aligning with intended sandbox usage: a custom class loader that loads untrusted classes into a constrained protection domain. This approach is more clearly correct and enables a self-protecting sandbox.
Note: this is true, but also a bit misleading. The reference to a "constrained protection domain" is to ClassLoader.defineClass, which can take a ProtectionDomain. The SecureClassLoader maps a ProtectionDomain to a CodeSource, and the URLClassLoader maps an array of URL to the individual CodeSource – the end result is that SecurityPolicy can see that a protection domain has a custom class loader, and can return different permissions from there.
Rather than attack the problem from inside the application, the team implemented a JVMTI agent which is even more secure, able to stop all the exploits they tried. It does have some drawbacks though:
Unfortunately, to use the JVMTI events that allow us to enforce rule 2, the JIT must be off, which drastically slows down the execution of applets in the presence of our agent. Rule 1 adds about 1-2% overhead.
I decided I would write my own sandbox. Jens Nordahl wrote a blog post called Sandboxing plugins in Java that was extraordinarily helpful in laying out the foundations.
Implementation
So, here's my example project. It starts up a sandbox, then tries to execute a script from within the sandbox. Add the execute permissions, and it works. Take out the execute permission, and it doesn't.
The main class is small:
object Main {
private val logger = LoggerFactory.getLogger(Main.getClass)
private val className = "com.tersesystems.sandboxexperiment.sandbox.ScriptRunner"
def main(args: Array[String]) {
val sm = System.getSecurityManager
if (sm == null) {
val homeDir = System.getProperty("user.dir")
val scriptName = s"${homeDir}/../testscript.sh"
logger.info("Starting security manager in the code")
System.setSecurityManager(new SandboxSecurityManager(scriptName))
} else {
logger.error(s"Predefined security manager ${sm}")
System.exit(-1)
}
try {
val result = runSandboxCode[String]
logger.info(s"result = $result")
} catch {
case e: AccessControlException =>
logger.error("Cannot run untrusted code", e)
case NonFatal(e) =>
logger.error("Unexpected error", e)
case other: Throwable =>
logger.error("Don't know what happened", other)
}
}
private def createSandboxClassLoader: SandboxClassLoader = {
DoPrivilegedAction(new RuntimePermission("createClassLoader")) {
new SandboxClassLoader(this.getClass.getClassLoader)
}(AccessController.getContext)
}
/**
* Uses reflection to instantiate the class which will try to execute shell code.
*/
private def runSandboxCode[T]: T = {
// Use a custom class loader to isolate the code...
val sandboxClassLoader = createSandboxClassLoader
val scriptRunnerClass = sandboxClassLoader.loadClass(className)
val method = scriptRunnerClass.getMethod("run")
val scriptRunnerInstance = scriptRunnerClass.newInstance()
try {
method.invoke(scriptRunnerInstance).asInstanceOf[T]
} catch {
case e: java.lang.reflect.InvocationTargetException =>
throw e.getCause
}
}
}
and the SandboxPolicy uses the classloader's type to determine the sandbox permissions:
class SandboxPolicy(scriptName: String) extends Policy {
override def getPermissions(domain: ProtectionDomain): PermissionCollection = {
val result: PermissionCollection = if (providerDomain == domain) {
new AllPermission().newPermissionCollection()
} else if (isSandbox(domain)) {
sandboxPermissions
} else {
appPermissions
}
result
}
private def sandboxPermissions: Permissions = {
val permissions = new Permissions()
permissions.add(new PropertyPermission("*", "read"))
// THIS IS THE LINE WHERE EVERYTHING HAPPENS!
// DON'T COMMENT OUT PLZ KTHXBYE
permissions.add(new java.io.FilePermission(scriptName, "execute"))
permissions
}
private def isSandbox(domain: ProtectionDomain): Boolean = {
domain.getClassLoader match {
case cl: SandboxClassLoader =>
true
case other =>
false
}
}
}
After that, the actual script runner is straightforward:
class ScriptRunner {
def run() = executeScript()
private def executeScript(): String = {
// Slightly hacked up because we don't have access to Main here...
val cwd = System.getProperty("user.dir")
val script = s"${cwd}/../testscript.sh"
// Okay, here we go!
val runtime = Runtime.getRuntime
val process = runtime.exec(script)
val input = new BufferedReader(new InputStreamReader(process.getInputStream))
val b = new StringBuffer()
for (line <- Iterator.continually(input.readLine()).takeWhile(_ != null)) {
b.append(line)
}
b.toString
}
}
I also added a bunch of logging, probably more than necessary. Any privilege escalation has to go through the SecurityManager, and so an exploit may stand a good chance of being detected even if it succeeds, especially once you add Logstash and an intrusion detection tool.
Experience and Limitations
The big limitation of the sandbox is that it is incredibly limiting. Java's sandbox is designed for completely untrusted code that can be downloaded from the Internet. It was not designed for protecting server-side code against privilege escalation exploits.
I found myself bumping into issues, mostly around what a security policy is capable of, and how plugin code must be packaged distinctly. There is no way to sandbox a particular class, for example – instead, you must identify a JAR or class location by URL, point a class loader at it, and then use reflection to instantiate the sandbox.
This means that to use a sandbox, there needs to be almost nothing on the classpath. From a comment on the article itself:
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.
Best Practices
For new applications, you can sandbox code pretty easily: you can write a small launcher that sets up the SecurityManager and a list of plugins, and sets up sandboxes for each of them.
Because you have a custom class loader, you do have some options. You can forbid loading of sensitive classes (such as serialization) by overriding loadClass:
override def loadClass(name: String, resolve: Boolean): Class[_] = {
if (forbiddenClasses.contains(name)) {
throw new IllegalArgumentException("This functionality is disabled")
}
super.loadClass(name, resolve)
}
Or you can assign individual classes to different CodeSource instances, and give them different ProtectionDomain areas inside the classloader itself, although that would require some tweaking to ensure that the class could only be found in the given CodeSource.
For refactoring an existing application, it is probably easier to generate a policy with pro-grade's policy generator: all the individual JAR files are known, so it's a matter of assigning them them the minimal set of permissions. It is incredibly easy to create a policy that allows the SecurityManager to be disabled, but the out of the box policy for Tomcat seems to avoid the dangerous permissions and has generally good guidelines.
If using a policy file is not practical, the next best option is to override the class loader to forbid or allow certain packages. This is what SecurityManager itself does internally – when you add permissions to access a particular package using the "accessClassInPackage" permission, it's actually feeding back into the class loader to do the check.
Further Reading
I have not covered a couple of areas. Notably, I haven't discussed what happens when the sandbox wants to create its own class loader, and how you would manage that securely. I imagine you'd look through the classloader's parents and if you see SandboxClassLoader in that list, you know it's sandboxed… but I don't know that there's not an exploit in that.
Likewise, I haven't gone into thread management very much. The AccessControlContext can be passed between different threads, but I don't have the mechanics down. There is an article about secure thread collaboration across protection domains. However, I have figured out how to prevent sandboxed code from spawning threads – something the default SecurityManager does not prevent by default.
There are still performance concerns about the SecurityManager, which was not designed for a heavily multi-threaded environment. However, there is progress in this area: Peter Firmstone and Alexey V. Varlamov wrote a drop in replacement for the security manager designed for concurrency as part of Apache River, and there's a completed project JEP-232 to improve secure application performance.
In addition to sandboxing, the SecurityManager can also be used to track JVM activity. Wealthfront added a modification to inspect IO in the JVM – the class itself is available at LessIOSecurityManager.java that can be set as a system property.
The ten year retrospective on Java Security by Li Gong makes for some interesting reading.
Other "orthodox" sandbox examples are thin on the ground. One example on Github is csm-sandbox, but I haven't checked it myself. You can also see a sandbox example in Java Security, 2nd Edition, in Appendix D.
It would be nice if the specification allowed a policy file to be packaged with a signed JAR file, so everything didn't have to be done in a single java.policy file that has to assign every single code source. For example, logback needs IO / network permissions, but does not need to execute scripts, so if it just came with a policy file that said so, it would be easy enough to incorporate that. Anything that minimizes the hassle of putting a policy file together would be a good thing.
Finally, JDK 1.9 is coming out soon. Modularization features should make isolation and boundaries easier to implement, and there's a reference to Module System Security Features in the slides.