I'm trying to prevent plugins that run "inside" the main Java application from accessing things they shouldn't. I've read about Policies, and AccessControllers, and ProtectionDomains, but they're very oriented around JARs.
I've tried this:
import java.nio.file.Files
import java.nio.file.Paths
import java.security.*
fun main(args: Array<String>) {
Policy.setPolicy(object : Policy() {})
System.setSecurityManager(SecurityManager())
val domain = ProtectionDomain(null, Permissions() /* no permissions */)
AccessController.doPrivileged(PrivilegedAction {
untrusted()
}, AccessControlContext(arrayOf(domain)))
}
fun untrusted() {
try {
// Works as expected
Files.readAllBytes(Paths.get("build.gradle"))
throw IllegalStateException("Was able to access file, but shouldn't have been able to")
} catch (e: AccessControlException) {
}
try {
// Should throw AccessControlException, but doesn't
AccessController.doPrivileged(PrivilegedAction {
Files.readAllBytes(Paths.get("build.gradle"))
})
throw IllegalStateException("Was able to access file, but shouldn't have been able to")
} catch (e: AccessControlException) {
}
}
Even though I'm invoking untrusted()
via a custom limited ProtectionDomain
, it seems it can trivially break out of it. I'm expecting the the doPrivileged
call in untrusted
to operate with the intersection of the permissions of the outermost ProtectionDomain
(the main program, which has all permissions) and the caller's ProtectionDomain
(which has no permissions), resulting in untrusted
having essentially 0 permissions.
I've also tried with the domain set like this:
val domain = ProtectionDomain(CodeSource(URL("http://foo"), null as Array<CodeSigner>?), Permissions() /* no permissions */)
but this also doesn't work -- the Policy
is queried with the main program's ProtectionDomain
and not the one calling untrusted()
. (Obviously I'd need to update the Policy
to handle "http://foo" correctly, but it doesn't even check that ProtectionDomain
anyway)
So where has my understanding gone wrong?
After doing some research on this, I think I have an answer. I could write a significantly longer answer, but I think I'll just cut to the chase here.
Each class loaded by a ClassLoader has a ProtectionDomain+CodeSource associated with it. These are somewhat coarse -- a CodeSource represents where a class came from, but it's not a pointer to an individual
.class
file or anything -- it's to a directory or a JAR. Thus two classes in the same JAR or directory will generally have identical permissions. Any class or script that has an identifiable ProtectionDomain+CodeSource can be whitelisted/blacklisted by your Policy.The exception (kinda) to this is, of course, is AccessController.doPrivileged with Permission arguments. This lets you clamp down the permissions of a region of code. But that code could, in theory, call
AccessController.doPrivileged
with just the callback. That method signature means "don't check my entire call stack for permissions; just look up my ProtectionDomain+CodeSource in the Policy file and see what it says." So if you're running truly untrusted code, you better make sure that a. it has a ProtectionDomain+CodeSource different from your trusted application, and b. that your Policy is able to identify that code and grant it appropriately-limited permissions.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.