Elad Benda
Elad Benda

Reputation: 36656

Using java securityManager blocks me from reading files

In my java code I call another 3rd party java class.

I want to catch that latter System.exit() exit code

So I use security-manager as suggested in this post

The problem is that I cannot read files now, I get permissions error

as seen in that post.

How can I catch the exit code and still read files?

Published class MyClass {

class MySecurityManager extends SecurityManager {
    @Override
    public void checkExit(int status) {
        throw new SecurityException();
    }
}

public void foo() {
    MySecurityManager secManager = new MySecurityManager();
    System.setSecurityManager(secManager);

    try {
        ConfigValidator.main(new String[]{"-dirs", SdkServiceConfig.s.PROPERTIES_FILE_PATH});

        new FileInputStream(new File("/Users/eladb/WorkspaceQa/sdk-service/src/main/resources/convert_conditions.sh"));


    } catch (SecurityException e) {
        //Do something if the external code used System.exit()
        String a = "1";

    }
    catch (Exception e) {

        logger.error("failed converting properties file to proto", e);
    }
}
}

Upvotes: 0

Views: 1958

Answers (1)

Uux
Uux

Reputation: 1218

You have two separate problems: Your trusted code cannot read the file, while the untrusted third-party library can still call System#exit unhindered. The former can be easily circumvented by granting further privileges to the trusted code; the latter is a tad trickier to address.

A bit of background

Privilege assignment
Code (the ProtectionDomains encapsulated by a thread's AccessControlContext) generally gets assigned Permissions in two ways: Statically, by the ClassLoader, upon class definition, and/or dynamically, by the Policy in effect. Other, less frequently encountered alternatives, exist as well: DomainCombiners, for instance, can modify AccessControlContexts' domains (and therefore the effective permissions of their respective code that is subject to authorization) on the fly, and custom domain implementations may use their own logic for permission implication, possibly disregarding or altering the semantics of the policy. By default the permission set of a domain is the union of its static and dynamic permissions. As for how exactly classes are mapped to domains, it is, for the most part, up to the loader's implementation. By default, all classes, JAR'ed or otherwise, residing beneath the same class path entry, are grouped under the same domain. More restrictive class loaders may choose to e.g. allocate a domain per class, which could be used to prevent communication even between classes within the same package.

Privilege evaluation
Under the default SecurityManager, for a privileged operation (an invocation of any method having a SecurityManager#checkXXX within its body) to succeed, every domain (of every class of every method) of the effective AccessControlContext must have been assigned, as explained above, the permission being checked. Recall however that the context need not necessarily represent "the truth" (the actual call stack)—system code gets optimized away early on, while AccessController#doPrivileged calls, along with the DomainCombiner potentially coupled to the AccessControlContext can modify the context's domains, and the authorization algorithm in its entirety, consequently.

Problem and workarounds

The issue with System#exit is that the corresponding permission (RuntimePermission("exitVM.*")) is one amongst few that are statically assigned by the default application class loader (sun.misc.Launcher$AppClassLoader) to all domains associated with classes loaded from the class path.

A number of alternatives come to mind:

  1. Installing a custom SecurityManager which denies the particular right based on, e.g., the class attempting to terminate the JVM process.
  2. Loading the third-party library from a "remote" location (a directory outside of the class path), so that it gets treated as "untrusted" code by its class loader.
  3. Authoring and installing a different application class loader, which does not assign the extraneous permission.
  4. Plugging a custom domain combiner into the access control context, which replaces, at the time of an authorization decision, all third-party domains with equivalent ones that do not have the offending permission.

I should, for the sake of completeness, note that at the Policy level, unfortunately, nothing can be done to negate statically assigned permissions.

The first option is overall the most convenient one, but I will not explore it further because:

  • The default SecurityManager is quite flexible, thanks to the handful of components (AccessController et al.) it interacts with. The background section in the beginning aimed to serve as a reminder of that flexibility, which "quick-n'-dirty" method overrides tend to cripple.
  • Careless modifications of the default implementation might cause (system) code to misbehave in curious ways.
  • Frankly, because it's boring―it's the one-size-fits-all solution perpetually advocated, while the fact that the default manager was standardized in 1.2 for a reason has long been forgotten.

The second alternative is easy to implement but impractical, complicating either development or the build process. Assuming you are not planning to invoke the library solely reflectively, or aided by interfaces present on the class path, it would have to be present initially, during development, and relocated before execution.

The third is, at least in the context of a standalone Java SE application, fairly straightforward and should not pose too much of a burden on performance. It is the approach I will favour herein.

The last option is the most novel and least convenient. It is hard to securely implement, has the greatest potential for performance degradation, and burdens client code with ensuring presence of the combiner prior to every delegation to untrusted code.

Proposed solution

The custom ClassLoader
The following is to be used as the replacement of the default application loader, or alternatively as the context class loader, or the loader used to load at least the untrusted classes. There is nothing novel to this implementation—all it does is prevent delegation to the default application class loader when the class in question is assumed to not be a system one. URLClassLoader#findClass, in turn, does not assign RuntimePermission("exitVM.*") to the domains of the classes it defines.

package com.example.trusted;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.regex.Pattern;

public class ClasspathClassLoader extends URLClassLoader {

    private static final Pattern SYSTEM_CLASS_PREFIX = Pattern.compile("((java(x)?|sun|oracle)\\.).*");

    public ClasspathClassLoader(ClassLoader parent) {
        super(new URL[0], parent);
        String[] classpath = System.getProperty("java.class.path").split(File.pathSeparator);
        for (String classpathEntry : classpath) {
            try {
                if (!classpathEntry.endsWith(".jar") && !classpathEntry.endsWith("/")) {
                    // URLClassLoader assumes paths without a trailing '/' to be JARs by default
                    classpathEntry += "/";
                }
                addURL(new URL("file:" + classpathEntry));
            }
            catch (MalformedURLException mue) {
                System.err.println(MessageFormat.format("Erroneous class path entry [{0}] skipped.", classpathEntry));
            }
        }
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class<?> ret;
        synchronized (getClassLoadingLock(name)) {
            ret = findLoadedClass(name);
            if (ret != null) {
                return ret;
            }
            if (SYSTEM_CLASS_PREFIX.matcher(name).matches()) {
                return super.loadClass(name, resolve);
            }
            ret = findClass(name);
            if (resolve) {
                resolveClass(ret);
            }
        }
        return ret;
    }

}

If you also wish to fine-tune the domains assigned to loaded classes, you will additionally have to override findClass. The following variant of the loader is a very crude attempt at doing so. constructClassDomain therein merely creates one domain per class path entry (which is more or less the default), but can be modified to do something different.

package com.example.trusted;

import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.ByteBuffer;
import java.security.AccessController;
import java.security.CodeSource;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public final class ClasspathClassLoader extends URLClassLoader {

    private static final Pattern SYSTEM_CLASS_PREFIX = Pattern.compile("((java(x)?|sun|oracle)\\.).*");
    private static final List<WeakReference<ProtectionDomain>> DOMAIN_CACHE = new ArrayList<>();

    // constructor, loadClass same as above

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        URL classOrigin = getClassResource(name);
        if (classOrigin == null) {
            return super.findClass(name);
        }
        URL classCodeSourceOrigin = getClassCodeSourceResource(classOrigin);
        if (classCodeSourceOrigin == null) {
            return super.findClass(name);
        }
        return defineClass(name, readClassData(classOrigin), constructClassDomain(classCodeSourceOrigin));
    }

    private URL getClassResource(String name) {
        return AccessController.doPrivileged((PrivilegedAction<URL>) () -> getResource(name.replace(".", "/") + ".class"));
    }

    private URL getClassCodeSourceResource(URL classResource) {
        for (URL classpathEntry : getURLs()) {
            if (classResource.getPath().startsWith(classpathEntry.getPath())) {
                return classpathEntry;
            }
        }
        return null;
    }

    private ByteBuffer readClassData(URL classResource) {
        try {
            BufferedInputStream in = new BufferedInputStream(classResource.openStream());
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            int i;
            while ((i = in.read()) != -1) {
                out.write(i);
            }
            return ByteBuffer.wrap(out.toByteArray());
        }
        catch (IOException ioe) {
            throw new RuntimeException(ioe);
        }
    }

    private ProtectionDomain constructClassDomain(URL classCodeSourceResource) {
        ProtectionDomain ret = getCachedDomain(classCodeSourceResource);
        if (ret == null) {
            CodeSource cs = new CodeSource(classCodeSourceResource, (Certificate[]) null);
            DOMAIN_CACHE.add(new WeakReference<>(ret = new ProtectionDomain(cs, getPermissions(cs), this, null)));
        }
        return ret;
    }

    private ProtectionDomain getCachedDomain(URL classCodeSourceResource) {
        for (WeakReference<ProtectionDomain> domainRef : DOMAIN_CACHE) {
            ProtectionDomain domain = domainRef.get();
            if (domain == null) {
                DOMAIN_CACHE.remove(domainRef);
            }
            else if (domain.getCodeSource().implies(new CodeSource(classCodeSourceResource, (Certificate[]) null))) {
                return domain;
            }
        }
        return null;
    }

}

The "unsafe" code

package com.example.untrusted;

public class Test {

    public static void testExitVm() {
        System.out.println("May I...?!");
        System.exit(-1);
    }

}

The entry point

package com.example.trusted;

import java.security.AccessControlException;
import java.security.Permission;

import com.example.untrusted.Test;

public class Main {

    private static final Permission EXIT_VM_PERM = new RuntimePermission("exitVM.*");

    public static void main(String... args) {
        System.setSecurityManager(new SecurityManager());
        try {
            Test.testExitVm();
        }
        catch (AccessControlException ace) {
            Permission deniedPerm = ace.getPermission();
            if (EXIT_VM_PERM.implies(deniedPerm)) {
                ace.printStackTrace();
                handleUnauthorizedVmExitAttempt(Integer.parseInt(deniedPerm.getName().replace("exitVM.", "")));
            }
        }
    }

    private static void handleUnauthorizedVmExitAttempt(int exitCode) {
        System.out.println("here let me do it for you");
        System.exit(exitCode);
    }

}

Testing

Packaging
Place the loader and the main class in one JAR (let's call it trusted.jar) and the demo untrusted class in another (untrusted.jar).

Assigning privileges
The default Policy (sun.security.provider.PolicyFile) is backed by the file at <JRE>/lib/security/java.policy, as well as any of the files referenced by the policy.url.n properties in <JRE>/lib/security/java.security. Modify the former (the latter should hopefully be empty by default) as follows:

// Standard extensions get all permissions by default

grant codeBase "file:${{java.ext.dirs}}/*" {
    permission java.security.AllPermission;
};

// no default permissions

grant {};

// trusted code

grant codeBase "file:///path/to/trusted.jar" {
    permission java.security.AllPermission;
};

// third-party code

grant codeBase "file:///path/to/untrusted.jar" {
    permission java.lang.RuntimePermission "exitVM.-1", "";
};

Note that it is virtually impossible to get components extending the security infrastructure (custom class loaders, policy providers, etc.) to work properly without granting them AllPermission.

Running
Run:

java -classpath "/path/to/trusted.jar:/path/to/untrusted.jar" -Djava.system.class.loader=com.example.trusted.ClasspathClassLoader com.example.trusted.Main

The privileged operation should succeed.

Next comment out the RuntimePermission under untrusted.jar, within the policy file, and re-run. The privileged operation should fail.

As a closing note, when debugging AccessControlExceptions, running with -Djava.security.debug=access=domain,access=failure,policy can help track down offending domains and policy configuration issues.

Upvotes: 2

Related Questions