1

I managed to set up sort of a "Java sandbox" with the following code:

    // #1
    new File("xxx").exists();

    // #2
    PrivilegedExceptionAction<Boolean> untrusted = () -> new File("xxx").exists();
    untrusted.run();

    // #3
    Policy.setPolicy(new Policy() {
        @Override public boolean implies(ProtectionDomain domain, Permission permission) { return true; }
    });
    System.setSecurityManager(new SecurityManager());

    AccessControlContext noPermissionsAccessControlContext;
    {
        Permissions noPermissions = new Permissions();
        noPermissions.setReadOnly();

        noPermissionsAccessControlContext = new AccessControlContext(
            new ProtectionDomain[] { new ProtectionDomain(null, noPermissions) }
        );
    }

    AccessControlContext allPermissionsAccessControlContext;
    {
        Permissions allPermissions = new Permissions();
        allPermissions.add(new AllPermission());
        allPermissions.setReadOnly();

        allPermissionsAccessControlContext = new AccessControlContext(
            new ProtectionDomain[] { new ProtectionDomain(null, allPermissions) }
        );
    }

    // #4
    try {
        AccessController.doPrivileged(untrusted, noPermissionsAccessControlContext);
        throw new AssertionError("AccessControlException expected");
    } catch (AccessControlException ace) {
        ;
    }

    // #5
    PrivilegedExceptionAction<Boolean> evil = () -> {
        return AccessController.doPrivileged(untrusted, allPermissionsAccessControlContext);
    };
    try {
        AccessController.doPrivileged(evil, noPermissionsAccessControlContext);
        throw new AssertionError("AccessControlException expected"); // Line #69
    } catch (AccessControlException ace) {
        ;
    }

#1 and #2 should be self-explanatory.

#3 is the code that sets up the sandbox: It sets a totally unrestictive Policy (otherwise we'd lock ourselves out immediately), and then a system SecurityManager.

#4 then executes the "untrusted" code in a totally restrictive AccessControlContext, which causes an AccessControlException, which is what I want. Fine.

Now comes #5, where some evil code attempts to "escape" from the sandbox: It creates another, totally unrestricted AccessControlContext, and runs the untrusted code within that. I would expect that this would throw an AccessControlException as well, because the evil code successfully leaves the sandbox, but it doesn't:

Exception in thread "main" java.lang.AssertionError: AccessControlException expected
at sbtest.Demo.main(Demo.java:69)

From what I read in the JRE JAVADOC

doPrivileged
public static <T> T doPrivileged(PrivilegedExceptionAction<T> action, AccessControlContext context)
     throws PrivilegedActionException

Performs the specified PrivilegedExceptionAction with privileges enabled and restricted by the
specified AccessControlContext. The action is performed with the intersection of the the permissions
possessed by the caller's protection domain, and those possessed by the domains represented by the
specified AccessControlContext.

If the action's run method throws an unchecked exception, it will propagate through this method.

Parameters:
    action -  the action to be performed
    context - an access control context representing the restriction to be applied to the caller's
              domain's privileges before performing the specified action. If the context is null,
              then no additional restriction is applied.
Returns:
    the value returned by the action's run method
Throws:
    PrivilegedActionException - if the specified action's run method threw a checked exception
    NullPointerException      - if the action is null
See Also:
    doPrivileged(PrivilegedAction), doPrivileged(PrivilegedExceptionAction,AccessControlContext)

, I would expect that the untrusted code would execute with no permissions and thus throw an AccessControlException, but that does not happen.

Why?? What can I do to mend that security hole?

Arno Unkrig
  • 181
  • 2
  • 5

1 Answers1

3

Well, the untrusted code does execute with no permissions initially... until it requests that its permissions get restored (nested doPrivileged call with AllPermission AccessControlContext). And that request gets honored, because, according to your Policy, all your code, including the "evil" action, is fully privileged. Put otherwise, doPrivileged is not an "on-demand sandboxing tool". It is only useful as a means for its immediate caller to limit or increase its privileges, within the confines of what has already been granted to it by the policy decision point (ClassLoader + SecurityManager/Policy). Callers further down the line are absolutely free to "revert" that change if entitled to -- once again according to the policy decision point, not the opinion of any previous caller. So this is as-intended behavior and not a security hole.

What workarounds are there?

For one, there certainly is a "canonical" / sane way of using the infrastructure. According to that best practice, trusted code is to be isolated from untrusted code by means of packaging and class loader, resulting in the two being associated with distinct domains that can be authorized individually. If the untrusted code were then only granted permission to, say, read from a particular file system directory, no amount of doPrivileged calls would enable it to, say, open a URL connection.

That aside, one may of course come up with a hundred and two alternatives of creatively (but not necessarily safely) utilizing the different moving pieces of the infrastructure to their advantage.

Here for example I had suggested a custom protection domain with a thread-local for accomplishing roughly what you desire, i.e., on-demand sandboxing of a normally privileged domain throughout the execution of an untrusted action.


Another way of selectively sandboxing code within a single protection domain is by establishing a default blacklist and using DomainCombiners to whitelist trustworthy execution paths. Note that it is only applicable when the SecurityManager is set programmatically.

First one needs to ensure that no permissions are granted by default, neither via ClassLoader1 nor by Policy2.

Then a "special" AccessControlContext, coupled to a domain combiner that unconditionally yields AllPermission, is obtained as follows:

// this PD stack is equivalent to AllPermission
ProtectionDomain[] empty = new ProtectionDomain[0];

// this combiner, if bound to an ACC, will unconditionally
// cause it to evaluate to AllPermission
DomainCombiner combiner = (current, assigned) -> empty;

// bind combiner to an ACC (doesn't matter which one, since
// combiner stateless); note that this call will fail under
// a security manager (will check SecurityPermission
// "createAccessControlContext")
AccessControlContext wrapper = new AccessControlContext(
    AccessController.getContext(), combiner);

// bind wrapper and thus combiner to current ACC; this will
// anew trigger a security check under a security manager.
// if this call succeeds, the returned ACC will have been
// marked "authorized" by the runtime, and can thus be
// reused to elevate permissions "on-demand" in the future
// without further a priori security checks.
AccessControlContext whitelisted = AccessController.doPrivileged(
    (PrivilegedAction<AccessControlContext>) AccessController::getContext,
    wrapper);

Now a standard security manager can be established to enforce the default blacklist. From this point onward all code will be blacklisted -- save for code holding a reference to the "whitelisted" / "backdoor" ACC, which it can leverage to escape the sandbox:

PrivilegedAction<Void> action = () -> {
    System.getSecurityManager().checkPermission(new AllPermission());
    return null;
};

// this will succeed, strange as it might appear
AccessController.doPrivileged(action, whitelisted);
// this won't
AccessController.doPrivileged(action);
// neither will this
action.run();

This leaves quite a bit of room for flexibility. One could directly call other trusted code within a "whitelisted" doPrivileged, and whitelisting would conveniently propagate up the call stack. One could alternatively expose the whitelisted ACC itself to trusted components so as to enable them to whitelist trusted execution paths of their own as they please. Or ACCs with limited permissions could be constructed, in the manner depicted above, and shared instead when it comes to code trusted less; or perhaps even specialized "pre-compiled" { action, limited pre-authorized ACC } capability objects.

Needless to say, whitelist propagation also widely opens up the door for bugs3. Unlike standard AccessController.doPrivileged(action) which is whitelist-opt-in, AccessController.doPrivileged(action, whitelisted) is effectively whitelist-opt-out; that is, for authorization to succeed under the former model, it is required that both the latest doPrivileged caller and every caller beyond it have the checked permission; whereas under the latter it suffices if merely the latest doPrivileged caller has it (provided no caller later on invokes standard doPrivileged, thereby reverting to the default blacklist).

Another prominent quirk4 to this approach lies with the fact that trusted third-party (library) code, reasonably expecting a call to standard doPrivileged to elevate its permissions, will be surprised to discover that it in fact causes the opposite.


1 The default application (aka "system") class loader only allows classes to read from their URL of origin (usually a JAR or directory on the local file system). Additionally permission to call System.exit is granted. If either is deemed "too much", a different class loader implementation will be necessitated.

2 Either by a custom subclass that unconditionally implies nothing, or by having the default sun.security.provider.PolicyFile implementation read an empty / "grant-nothing" configuration ("grant {};").

3 The convenient yet dangerous property of whitelist propagation could in theory be countered by use of a stateful combiner that emulates the behavior of standard doPrivileged by means of call stack inspection. Such a combiner might be provided a stable offset from the call stack's bottom upon instantiation, representing the invocation of some doWhitelistedSingleFrame(PrivilegedAction, Permission...) utility method exposed to trustworthy clients. Upon invocation of its combine method due to permission checks subsequently, the combiner would ensure that the stack has not gotten any deeper than offset + 1 (skipping frames associated with SecurityManager, AccessController, AccessControlContext, and itself), and, if it indeed hasn't, yield a modified PD stack evaluating to the desired permissions, while otherwise reverting to the default blacklist. Of course the combiner would have to be prepared to encounter synthetic frames (e.g. lambdas and bridges) at the anchored offset. Additionally it would have to safeguard itself from being used "out of context", via leakage to a different ACC or thread; and it would have to self-invalidate upon the utility method's return.

4 The only straightforward solution here would be to assign permissions to such code in the usual fashion (class loader and/or policy) -- which kind of defeats the goal of the exercise (circumventing the need for separate domains / class loaders / packaging).

Uux
  • 1,218
  • 1
  • 10
  • 21