How do I create a Java sandbox?

JavaSecurityPluginsSandbox

Java Problem Overview


I want to make my application to run other people's code, aka plugins. However, what options do I have to make this secure so they don't write malicious code. How do I control what they can or can not do?

I have stumbled around that JVM has a "built in sandbox" feature - what is it and is this the only way? Are there third-party Java libraries for making a sandbox?

What options do I have? Links to guides and examples is appreciated!

Java Solutions


Solution 1 - Java

You are looking for a security manager. You can restrict the permissions of an application by specifying a policy.

Solution 2 - Java

  • Defining and registering your own security manager will allow you to limit what the code does - see oracle documentation for SecurityManager.

  • Also, consider creating a separate mechanism for loading the code - i.e. you could write or instantiate another Classloader to load the code from a special place. You might have a convention for loading the code - for example from a special directory or from a specially formatted zip file (as WAR files and JAR files). If you're writing a classloader it puts you in the position of having to do work to get the code loaded. This means that if you see something (or some dependency) you want to reject you can simply fail to load the code. http://java.sun.com/javase/6/docs/api/java/lang/ClassLoader.html

Solution 3 - Java

Have a look at the java-sandbox project which allows to easily create very flexible sandboxes to run untrusted code.

Solution 4 - Java

For an AWT/Swing application you need to use non-standard AppContext class, which could change at any time. So, to be effective you would need to start another process to run plug-in code, and deal with communication between the two (a little like Chrome). The plug-in process will need a SecurityManager set and a ClassLoader to both isolate the plug-in code and apply an appropriate ProtectionDomain to plug-in classes.

Solution 5 - Java

Here's how the problem can be solved with a SecurityManager:

https://svn.code.sf.net/p/loggifier/code/trunk/de.unkrig.commons.lang/src/de/unkrig/commons/lang/security/Sandbox.java

package de.unkrig.commons.lang.security;

import java.security.AccessControlContext;
import java.security.Permission;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

import de.unkrig.commons.nullanalysis.Nullable;

/**
 * This class establishes a security manager that confines the permissions for code executed through specific classes,
 * which may be specified by class, class name and/or class loader.
 * <p>
 * To 'execute through a class' means that the execution stack includes the class. E.g., if a method of class {@code A}
 * invokes a method of class {@code B}, which then invokes a method of class {@code C}, and all three classes were
 * previously {@link #confine(Class, Permissions) confined}, then for all actions that are executed by class {@code C}
 * the <i>intersection</i> of the three {@link Permissions} apply.
 * <p>
 * Once the permissions for a class, class name or class loader are confined, they cannot be changed; this prevents any
 * attempts (e.g. of the confined class itself) to release the confinement.
 * <p>
 * Code example:
 * <pre>
 *  Runnable unprivileged = new Runnable() {
 *      public void run() {
 *          System.getProperty("user.dir");
 *      }
 *  };
 *
 *  // Run without confinement.
 *  unprivileged.run(); // Works fine.
 *
 *  // Set the most strict permissions.
 *  Sandbox.confine(unprivileged.getClass(), new Permissions());
 *  unprivileged.run(); // Throws a SecurityException.
 *
 *  // Attempt to change the permissions.
 *  {
 *      Permissions permissions = new Permissions();
 *      permissions.add(new AllPermission());
 *      Sandbox.confine(unprivileged.getClass(), permissions); // Throws a SecurityException.
 *  }
 *  unprivileged.run();
 * </pre>
 */
public final
class Sandbox {

    private Sandbox() {}

    private static final Map<Class<?>, AccessControlContext>
    CHECKED_CLASSES = Collections.synchronizedMap(new WeakHashMap<Class<?>, AccessControlContext>());

    private static final Map<String, AccessControlContext>
    CHECKED_CLASS_NAMES = Collections.synchronizedMap(new HashMap<String, AccessControlContext>());

    private static final Map<ClassLoader, AccessControlContext>
    CHECKED_CLASS_LOADERS = Collections.synchronizedMap(new WeakHashMap<ClassLoader, AccessControlContext>());

    static {

        // Install our custom security manager.
        if (System.getSecurityManager() != null) {
            throw new ExceptionInInitializerError("There's already a security manager set");
        }
        System.setSecurityManager(new SecurityManager() {

            @Override public void
            checkPermission(@Nullable Permission perm) {
                assert perm != null;

                for (Class<?> clasS : this.getClassContext()) {

                    // Check if an ACC was set for the class.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASSES.get(clasS);
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class name.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_NAMES.get(clasS.getName());
                        if (acc != null) acc.checkPermission(perm);
                    }

                    // Check if an ACC was set for the class loader.
                    {
                        AccessControlContext acc = Sandbox.CHECKED_CLASS_LOADERS.get(clasS.getClassLoader());
                        if (acc != null) acc.checkPermission(perm);
                    }
                }
            }
        });
    }

    // --------------------------

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * accessControlContext}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, AccessControlContext accessControlContext) {

        if (Sandbox.CHECKED_CLASSES.containsKey(clasS)) {
            throw new SecurityException("Attempt to change the access control context for '" + clasS + "'");
        }

        Sandbox.CHECKED_CLASSES.put(clasS, accessControlContext);
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * protectionDomain}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, ProtectionDomain protectionDomain) {
        Sandbox.confine(
            clasS,
            new AccessControlContext(new ProtectionDomain[] { protectionDomain })
        );
    }

    /**
     * All future actions that are executed through the given {@code clasS} will be checked against the given {@code
     * permissions}.
     *
     * @throws SecurityException Permissions are already confined for the {@code clasS}
     */
    public static void
    confine(Class<?> clasS, Permissions permissions) {
        Sandbox.confine(clasS, new ProtectionDomain(null, permissions));
    }

    // Code for 'CHECKED_CLASS_NAMES' and 'CHECKED_CLASS_LOADERS' omitted here.

}

Solution 6 - Java

The discussion on this question inspired me to start up my own sandbox project.

https://github.com/Black-Mantha/sandbox

In it I've come across an important security question: "How do you allow the code outside the sandbox to bypass the SecurityManager?"

I put the sandbox code in its own ThreadGroup, and always grant permission when outside that group. If you need to run privileged code in that group anyway (in a callback, for example), you can use a ThreadLocal to set a flag for that Thread only. The classloader will prevent the sandbox from accessing the ThreadLocal. Also, if you do this you need to forbid the use of finalizers, since they run in a dedicated thread outside the ThreadGroup.

Solution 7 - Java

After spending a day in the depths of the Java security APIs, I found an amazingly simple solution for executing untrusted code within a sandbox confined by Permissions:

https://github.com/janino-compiler/janino/blob/master/commons-compiler/src/main/java/org/codehaus/commons/compiler/Sandbox.java

Here's the (simplified) source code:

package org.codehaus.commons.compiler;
 
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Policy;
import java.security.PrivilegedAction;
import java.security.PrivilegedActionException;
import java.security.PrivilegedExceptionAction;
import java.security.ProtectionDomain;
 
public final
class Sandbox {
 
    static {
 
        if (System.getSecurityManager() == null) {
 
            // Before installing the security manager, configure a decent ("positive") policy.
           Policy.setPolicy(new Policy() {
 
                @Override public boolean
                implies(ProtectionDomain domain, Permission permission) { return true; }
            });
 
            System.setSecurityManager(new SecurityManager());
        }
    }
 
    private final AccessControlContext accessControlContext;
 
    /**
     * @param permissions Will be applied on later calls to {@link #confine(PrivilegedAction)} and {@link
     *                    #confine(PrivilegedExceptionAction)}
     */
    public
    Sandbox(PermissionCollection permissions) {
        this.accessControlContext = new AccessControlContext(new ProtectionDomain[] {
            new ProtectionDomain(null, permissions)
        });
    }
 
    /**
     * Runs the given <var>action</var>, confined by the permissions configured through the {@link
     * #Sandbox(PermissionCollection) constructor}.
     *
     * @return The value returned by the <var>action</var>
     */
    public <R> R
    confine(PrivilegedAction<R> action) {
        return AccessController.doPrivileged(action, this.accessControlContext);
    }
 
    public <R> R
    confine(PrivilegedExceptionAction<R> action) throws Exception {
        try {
            return AccessController.doPrivileged(action, this.accessControlContext);
        } catch (PrivilegedActionException pae) {
            throw pae.getException();
        }
    }
}

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestioncorgrathView Question on Stackoverflow
Solution 1 - JavatangensView Answer on Stackoverflow
Solution 2 - JavaDafydd ReesView Answer on Stackoverflow
Solution 3 - JavaArno MittelbachView Answer on Stackoverflow
Solution 4 - JavaTom Hawtin - tacklineView Answer on Stackoverflow
Solution 5 - JavaArno UnkrigView Answer on Stackoverflow
Solution 6 - JavaBlack ManthaView Answer on Stackoverflow
Solution 7 - JavaArno UnkrigView Answer on Stackoverflow