TL;DR: Code
The question is essentially
"How do I invoke a method on a particular instance, with privileges lower than normal?". There are three requirements here:
- Code is to be authorized on a per-instance basis. An instance is privileged by default.
- An instance may be selectively blacklisted, i.e., it may be accorded lower privileges than it normally would have been, for the duration of a method invocation that it receives.
Blacklisting must propagate to code executed on the receiver's behalf, specifically any objects of the same type that it interacts with, itself included; otherwise, if, say, the receiver were in turn to call
AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
this.doSomethingElse();
return null;
});
doSomethingElse()
would escape the sandbox.
All three are problematic:
- The first one is not really1 achievable, because it presupposes that the runtime maintain—and expose—information about the instances (rather than merely the classes) on threads' execution stacks, which it does not2.
- The second and third are only achievable as long as any blacklisted code does not assert its own (default, class-based) privileges via
AccessController.doPrivileged(...)
, which, by design, it may at any time choose to.
Is there an alternative?
Well, how far are you willing to go? Modify AccessController
/ AccessControlContext
internals? Or worse yet, internals of the VM? Or perhaps provide your own SecurityManager
that reimplements the aforementioned components' functionality from scratch, in a way that satisfies your requirements? If the answer to all is "no", then I fear that your options are limited.
As an aside, you should ideally be able to make a binary choice when asked "Can or cannot this particular code, i.e. class, be entrusted with the particular privileges?", for this would tremendously simplify3 things. Unfortunately you cannot; and, to make matters worse, neither can you, presumably, modify the implementation of the class such that all of its instances can either be considered—with regards to a specific set of privileges—trustworthy or not, nor do you wish to simply mark the class, and therefore all of its instances, as untrusted (which I do believe you should!) and live with it.
Moving on to the proposed workaround. To overcome the shortcomings listed earlier, the original question will be rephrased as follows: "How do I invoke a method with elevated privileges accorded to the method receiver's ProtectionDomain
?" I am going to answer this derivative question instead, suggesting, in contrast to the original one, that:
- Code is to be authorized by the
ProtectionDomain
of its class, as is normally the case. Code is sandboxed by default.
- Code may be selectively whitelisted, for the duration of a method invocation under a particular thread.
- Whitelisting must propagate4 to code of the same class called by the receiver.
The revised requirements will be satisfied by means of a custom ClassLoader
and DomainCombiner
. The purpose of the first is to assign a distinct ProtectionDomain
per class5; the other's is to temporarily replace the domains of individual classes within the current AccessControlContext
for "on-demand whitelisting" purposes. The SecurityManager
is additionally extended to prevent thread creation by unprivileged code4.
Note: I relocated the code to
this gist to keep the post's length below the limit.
Standard disclaimer:
Proof-of-concept code—take with several tablespoons of salt!
Running the example
Compile and deploy the code as suggested by the example policy configuration file, i.e., there should be two6 unrelated classpath entries (e.g. sibling directories at the filesystem level)—one for classes of the com.example.trusted
package, and another for com.example.untrusted.Nasty
.
Ensure also that you have replaced the policy configuration with the example one, and have modified the paths therein as appropriate.
Lastly run (after having appropriately modified the classpath entries, of course):
java -cp /path/to/classpath-entry-for-trusted-code:/path/to/classpath-entry-for-untrusted-code -Djava.system.class.loader=com.example.trusted.DiscriminatingClasspathClassLoader com.example.trusted.Main
The first call to the untrusted method should hopefully succeed, and the second fail.
1 It would perhaps be possible for instances of a specially crafted class (having, e.g., a domain of their own, assigned by some trusted component) to exercise their own privileges themselves (which does not hold true in this case, since you have no control over the implementation of instance
's class, it appears). Nevertheless, this would still not satisfy the second and third requirement.
2 Recall that, under the default SecurityManager
, a Permission
is granted when all ProtectionDomain
s—to which normally classes, rather than instances, are mapped—of the thread's AccessControlContext
imply that permission.
3 You would then simply have to grant permissions at the policy level, if you deemed the class trustworthy, or otherwise grant nothing at all, rather than have to worry about permissions per instance per security context and whatnot.
4 This is a hard decision: If whitelisting did not affect subsequent callees of the same type, the instance would not be able to call any privilege-requiring methods on itself. Now that it does, on the other hand, any other instance of the same type, that the original whitelisted method receiver interacts with, become privileged too! Thus you must ensure that the receiver does not call any "untrusted" instances of its own kind. It is for the same reason a bad idea to allow the receiver to spawn any threads.
5 As opposed to the strategy employed by the default application ClassLoader
, which is to group all classes that reside under the same classpath entry within a single ProtectionDomain
.
6 The reason for the inconvenience is that the ProtectionDomain
, which our custom application ClassLoader
's class gets mapped to by its parent, has a CodeSource
implying all CodeSource
s referring to files under the loader's classpath entry. So far so good. Now, when asked to load a class, our loader attempts to discern between system/extension classes (loading of which it delegates to its parent) and application classes, by testing whether the .class file is located below JAVA_HOME
. Naturally, to be allowed to do so, it requires read access to the filesystem subtree beneath JAVA_HOME
. Unfortunately, granting the corresponding privilege to our loader's (notoriously broad) domain, implicitly grants the privilege to the domains of all other classes residing beneath the loader's classpath entry, including untrusted ones, as well. And that should hopefully explain why classpath entry-level isolation between trusted and untrusted code is necessary. There are of course workarounds, as always; e.g. mandating that trusted code be additionally signed in order to accrue any privileges; or perhaps using a more flexible URL scheme for code source identification, and/or altering code source implication semantics.
Further reading:
Historical note: Originally this answer proposed a nearly identical solution that
abusedrelied on JAAS's
SubjectDomainCombiner
, rather than a custom one, for dynamic privilege modification. A "special"
Principal
would be attached to specific domains, which would then accrue additional
Permission
s upon evaluation by the
Policy
, based on their composite
CodeSource
-
Principal
identity.