When it comes time to host some partially trusted code in your application, perhaps as a part of an Add-In model, you’ve got a few options to choose from. How do you decide which is the best way to go?
Thankfully the answer to this one is relatively straightforward – choose the new simple sandboxing model whenever possible. Here’s 5 good reasons to prefer it over other options:
1. It provides an effective sandbox
We’ve already talked about why using PermitOnly or Deny to sandbox code is not an effective option. The primary issue is that both of these are simply stack walk modifiers, and have no effect on the grant set of any assembly. Because of that, they can be overridden with an Assert in the sandboxed assembly. The sandboxed code (and I’m using that term very loosly in this context), won’t even have to Assert to satisfy a LinkDemand or an InheritenceDemand since those are done at JIT time and do not involve a stack walk.
In addition to being ineffective against various CAS demands, these stack walk modifiers make life much harder when it comes to protecting sensitive information. Since static variables are shared on an AppDomain level, any static state that your application may maintain needs to be protected against these “sandboxed” assemblies. If you were to create a new AppDomain to sandbox in, the shared state problem goes away because each AppDomain gets a new copy of the static data.
Since these stack modifiers obviously cannot create an effective sandbox, we’re left with two options, the legacy sandboxed domain using a v1.x AppDomain policy level, and the new v2.0 simple sandboxing API. The next four points show why it’s generally preferable to pick the v2.0 model if you don’t have to run your code on the v1.x runtimes.
2. Simpler to Create
Even for the most basic of sandboxes, a simple sandbox takes about 50% less code to create than the v1.x sandbox – and reducing the number of lines of security code in your application is goodness.
Not only are lines of code reduced, but the code itself is much easier to read and understand. The input to the simple sandboxing API is a single permission set to grant all partially trusted assemblies (and the domain itself), along with a list of assemblies to grant FullTrust (presumably assemblies provided by your hosting application).
On the other hand, the input to the legacy sandboxing API was a policy tree and some evidence. In order to figure out what permissions each assembly is going to get within the domain, you need to draw out the tree and evaluate its evidence against the policy. The same goes for the AppDomain itself. Of course, to do this, you need to understand the various evidences that an Assembly might have. Does your hosting application provide custom evidence? Does it remove or add evidence that the CLR provided? Exactly which evidence will the CLR be providing for each assembly anyway?
And, once you’ve determined all that, you’ve only figured out the maximum permission set that an assembly will have. Since the AppDomain policy level is intersected with the other three policy levels, you might end up with fewer permissions than you though.
Getting the policy tree right in the first place can also be deceptively tricky. What membership condition do you want? Which merge logic for child code groups? What parent code groups should be added?
When doing a code review, the upper hand is clearly to the simple sandboxing API on this front, since it’s very clear what the permission sets to be granted to each assembly are. In the legacy model, you can’t determine that just by looking at the code alone, since you’ll need to know the evidence granted to every one of the assemblies to be loaded into the domain.
Due to the potential complexities involved, getting the policy tree correct can be difficult, whereas setting up a simple sandbox is relatively straightforward.
3. Simpler to Understand and Reason About
I touched on this a bit in the previous point. The “simple” in the simple sandboxing API isn’t just about the code needed to create the domain. It’s also about making it easier to reason about the domain and code that runs in it.
Since a legacy sandboxed domain can have arbitrary policy, with arbitrary grant sets to every individual assembly loaded into it, you run into several complexities. The first one is just making sure that you’ve actually correctly calculated the grant set for each assembly. You also need to make sure that whatever evidence that you supplied the AppDomain will provide a secure enough backdrop for any demands that happen to hit that boundary.
Assuming that you’ve got the various grant sets of the assemblies figured out, you now have to keep each of these sets in mind when coding and reviewing any code that runs in the sandbox. Does it work correctly when called by each of the other permission sets floating around? Any code that’s being called by code with a lower permission set needs to be designed and written so that it cannot be tricked into elevating the lower permission set to its own permission set – something that can be surprisingly tricky. (Although transparent code helps you out a ton here).
When you use the simple sandboxing API, you’ve got exactly two permission sets floating around your domain, the sandboxed permission set and FullTrust. (Of course, nothing is preventing you from making the sandboxed set FullTrust so that you have only one set, although that’s not really sandboxing as much as separating code for reliability’s sake). Having just these two permission sets means that you know the core set of code which needs to worry about elevation attacks. It also means that you know the backstop of the AppDomain boundary is a secure one for your domain.
4. Resilient to Policy Changes
Since in the legacy sandboxing model, the final permission set was determined by intersecting all four policy levels, some applications would make policy decisions in a level other than the AppDomain level. Depending on your application, this might have been the only choice for you – VSTO is the canonical example here.
The problem comes into play when you realize that the other three policy levels are shared resources for the machine, and there could be contention over them. For instance, on popular way to create and deploy modifications to policy is to have one of the developers use the MMC snap in that ships with the framework SDK to create a policy MSI file.
These MSI files are snapshots of the current state of policy however, and contain no merge logic at all. This means that if two applications deploy policy in this way, the last one to install wins; the first application will have its changes removed from the system. If that application was not written with this situation in mind, it probably will fail with unhandled SecurityExceptions, causing a less than ideal user experience. In fact, given that the first application might not be run for some time after the second is installed, it could be very difficult to trace back the cause of failure!
IT departments also run into this problem; they need to deploy policy changes across the company for various internal applications. One way to solve the situation is to create one MSI file that contains all the policy changes for every application. However, that’s not ideal since users will end up with policy entries for applications they might never need to run. Another solution is to generate a script with the appropriate caspol logic; something that can be difficult to do properly.
5. Resilient to CLR Versioning
This is really a special case of being resilient to policy changes. Jessie Kaplan discusses an issue we ran into with VSTO with the v2.0 upgrade in his recent MSDN column. Since VSTO made its policy decisions in the v1.1 security policy, and security policy is tied to a specific version of the runtime, when v2.0 was installed on a machine all of the VSTO policy modifications were essentially lost to it.
Your application can protect itself against this issue by providing a config file that directs it to bind against one specific CLR instead of floating forward to a newer compatible version. This does provide a requirement that the users of your application keep the older CLR around, and may cause more pressure on you to provide a newer application built against the newer runtime.
Both of the above issues are solved for standalone applications with the advent of ClickOnce in v2.0. Now trust decisions are tied to the application, instead of the CLR version or the specific policy. If one application installs, it’s not going to affect the trust decisions for any other application on the machine. Administrators can still exert some control over these trust decisions by using a set of registry keys to control the trust manager. It’s no coincidence that under the covers, ClickOnce is using the simple sandboxing model to ensure that its applications get run with exactly the permissions they require.