Here is one way for the example to run as intended, i.e., to effectively blacklist subsequent execution paths under the same domain. The core permission-intersection-based authorization model should still hold. The sample must be run with -Djava.system.class.loader=com.example.Test$AppClassLoader
(this replacement system class loader is only needed in order to attain a working single-file example).
Obligatory disclaimer: While technically many things are possible, to the point of dynamically white-/blacklisting individual instances and beyond, they all involve additional context of some sort being introduced into the already non-trivial authorization process. Such approaches should be avoided whenever possible. The proper solution, sufficing in the vast majority of cases, as documented in the OP's answer's conclusion, is to package trusted code separately from untrusted (and, when manually managing class-to-domain mappings, ensuring that code bases of distinct trustworthiness are mapped to distinct domains), and assign appropriate permissions to the resulting domains.
package com.example;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.AccessControlException;
import java.security.AccessController;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Permissions;
import java.security.PrivilegedAction;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class Test {
public static final class AppClassLoader extends URLClassLoader {
private static final URL[] CLASS_PATH;
private static final String SANDBOXABLE_DOMAIN_CLASS_NAME = "com.example.Test$SandboxableDomain";
static {
String[] paths = System.getProperty("java.class.path").split(File.pathSeparator);
List<URL> classPath = new ArrayList<>();
for (String path : paths) {
try {
classPath.add(new URL("file://" + path));
}
catch (MalformedURLException ex) {}
}
CLASS_PATH = classPath.toArray(new URL[0]);
}
private final Constructor<?> sandboxableDomainCtor;
{
try {
// ensure this loader defines SandboxableDomain so that normal code
// can safely / conveniently access it via class literal
Class<?> sandboxableDomainClass = loadClass(SANDBOXABLE_DOMAIN_CLASS_NAME, true);
sandboxableDomainCtor = sandboxableDomainClass.getConstructor(CodeSource.class,
PermissionCollection.class, ClassLoader.class);
}
catch (ReflectiveOperationException ex) {
throw new RuntimeException(ex);
}
}
public AppClassLoader(ClassLoader parent) {
super(CLASS_PATH, parent);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (name.startsWith("java") || name.startsWith("sun")) {
return super.loadClass(name, resolve);
}
Class<?> ret = findLoadedClass(name);
if (ret != null) {
return ret;
}
ProtectionDomain assignedDomain;
byte[] classData;
try {
URL classResource = getResource(name.replace(".", "/") + ".class");
CodeSource assignedCodeSource = new CodeSource(classResource, (Certificate[]) null);
classData = Files.readAllBytes(Paths.get(classResource.toURI()));
if (SANDBOXABLE_DOMAIN_CLASS_NAME.equals(name)) {
// loading the domain class itself; ensure _its own_ domain is fully privileged,
// so that it doesn't affect authorization
PermissionCollection perms = new Permissions();
perms.add(new AllPermission());
assignedDomain = new ProtectionDomain(assignedCodeSource, perms, this, null);
}
else {
// the per-class code source (URL) is unintentional; normally all classes under
// the same class path entry would share one
assignedDomain = (ProtectionDomain) sandboxableDomainCtor.newInstance(assignedCodeSource,
getPermissions(assignedCodeSource), this);
}
}
catch (NullPointerException | URISyntaxException | IOException | ReflectiveOperationException ex) {
throw new ClassNotFoundException(name);
}
ret = defineClass(name, classData, 0, classData.length, assignedDomain);
if (resolve) {
resolveClass(ret);
}
return ret;
}
}
public static final class SandboxableDomain extends ProtectionDomain {
private static final Permission DO_SANDBOXED_PERM = new RuntimePermission("com.example.doSandboxed");
private final ThreadLocal<Boolean> sandboxed = new InheritableThreadLocal<>();
public SandboxableDomain(CodeSource cs, PermissionCollection permissions, ClassLoader classLoader) {
super(cs, permissions, classLoader, null);
sandboxed.set(false);
}
// no equivalent doUnsandboxed here for escaping the sandbox on-demand;
// firstly because it's fishy; secondly because it would be impossible
// to distinguish a privileged caller based on permissions alone
public void doSandboxed(Runnable action) {
if (!sandboxed.get()) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(DO_SANDBOXED_PERM);
}
}
sandboxed.set(true);
try {
action.run();
}
finally {
sandboxed.set(false);
}
}
@Override
public boolean implies(Permission permission) {
if (sandboxed.get()) {
// static only (AppClassLoader only grants essentials like reading from own directory)
PermissionCollection perms = getPermissions();
return (perms == null) ? false : perms.implies(permission);
}
// static + policy
return super.implies(permission);
}
}
public static void main(String[] args) throws Exception {
initSecurity();
SandboxableDomain ownDomain = (SandboxableDomain) Test.class.getProtectionDomain();
System.out.println("Try unsandboxed"); // should succeed
untrusted();
System.out.println("---\n\nTry sandboxed"); // should fail
ownDomain.doSandboxed(Test::untrusted);
System.out.println("---\n\nTry unsandboxed from within a child thread"); // should succeed
new Thread(Test::untrusted).start();
Thread.sleep(1000);
System.out.println("---\n\nTry unsandboxed from within a sandboxed child thread"); // should fail
ownDomain.doSandboxed(() -> new Thread(Test::untrusted).start());
}
private static void initSecurity() throws Exception {
Path tempPolicyConfig = Files.createTempFile(null, null);
// self-grant AllPermission
Files.write(tempPolicyConfig,
Collections.singletonList(new StringBuilder("grant codebase \"")
.append(Test.class.getProtectionDomain().getCodeSource().getLocation()).append("\"{permission ")
.append(AllPermission.class.getName()).append(";};").toString()));
System.setProperty("java.security.policy", "=" + tempPolicyConfig.toString());
System.setSecurityManager(new SecurityManager());
Files.delete(tempPolicyConfig);
}
private static void untrusted() {
try {
untrusted0();
System.out.println("\tSucceeded");
}
catch (AccessControlException ex) {
System.out.println("\tFailed; try via doPrivileged");
try {
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
untrusted0();
return null;
});
System.out.println("\t\tSucceeded");
}
catch (AccessControlException ex1) {
System.out.println("\t\tFailed anew");
}
}
}
private static void untrusted0() {
try {
Files.readAllBytes(Paths.get("build.gradle"));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}