Recently I developed a lab for our Writing Secure Code – ASP.NET training course where students modify Hacme Bank to run in Partial Trust rather than Full Trust.
A lot has been written about Partial Trust. It's not going to solve every security problem, but it's a smart thing to do. I wanted to show students that it was easy to take an existing application and get it to run with only the privileges it needed.
Turns out, there is more than one way to skin a cat. And, depending on your architecture, you may be spinning your wheels needlessly, as I learned the hard way.
Hacme Bank is based on the older .asmx web service architecture, with the web front-end calling a service layer, which calls the database.
If we configure the site to run in the default Medium Trust level, it does not have access to the Hacme Bank web service (a WebPermission error is thrown).
After a few hours of tinkering, reading, debugging, and throwing my innocent wireless mouse across the room (my preferred method of stress management), I discovered a couple of different methods that I could use to get this working. Thanks to Dominick Baier and Rudolph Araujo for seeding the clouds of this brainstorm.
The four options are:
- Set the originUrl attribute in the trust element of the web.config file
- Create a new custom trust level
- Partition the privileged code into an assembly and install in the Global Assembly Cache
- Partition the privileged code into an assembly and create a new custom trust level
And, for those of you who aren't into reading longish blog posts, here's a summary of what I found:
|
Approach
|
Pros
|
Cons
|
|
Set the originUrl attribute in the trust element of the web.config file
|
Really easy
|
Only works for web permissions (like calling a web service)
|
|
Create a new custom trust level
|
Only necessary permissions are granted
|
All code runs with the extra permissions
|
|
Partition the privileged code into an assembly and install in the Global Assembly Cache
|
Only a small amount of code gets elevated privileges
|
The code that gets elevated privileges runs in Full Trust
|
|
Partition the privileged code into an assembly and create a new custom trust level
|
Only necessary permissions are granted and only a small amount of code gets elevated privileges
|
Difficult
|
Now for some background:
1. Set the originUrl attribute in the trust element of the web.config file.
originUrl is an optional attribute that punches a hole in medium trust, allowing web connections to hosts defined by a regular expression. This is used to facilitate exactly what we need – connecting to a web service at an arbitrary location.
This is implemented by an entry in the web_mediumtrust.config file:
<IPermission class="WebPermission" version="1">
<ConnectAccess>
<URI uri="$OriginHost$"/>
</ConnectAccess>
</IPermission>
Besides making this lab a little too easy, the originUrl attribute is limited to web permissions and is not useful for different permission elevation scenarios such as accessing the registry or using reflection.
2. Create a new custom trust level.
A more flexible approach is to create a new trust level. Usually this is done by copying over one of the policy files and adding the necessary permission. A good description is available at the Patterns and Practices site here.
While this approach gives us the tools to add arbitrary permissions to the code base, it doesn't exactly follow the principal of least privilege, because all of the code in the web site is granted the permissions, rather than just the code that requires them.
3. Partition the privileged code into an assembly and install in the Global Assembly Cache.
In order to segregate duties and reduce the overall privileges granted, we can rip out the code that needs permissions and throw it in to a new assembly. This is also known as sandboxing. All code that can run with low privileges is in a very low privilege environment, and is given limited access to higher privilege functionality. This approach has some similarity with the Silverlight security model and the security safe critical attribute – I'll blog about this later.
Once we have our privileged code in a separate assembly, we need to configure our environment so that the privileged code can be accessed safely. First, the privileged assembly should be decorated with the AllowPartiallyTrustedCallersAttribtue, strong nam signed, and (in this case) installed in the GAC. To protect the code from being accessed by other classes, we can add a Code Access Security demand that will prevent access by all but the required classes. We have a few options here:
- Use a StrongNameIdentityPermission. If we strong name the ASP.NET application (running with partial trust), we can verify the strong name in the privileged assembly. This, unfortunately, isn't as easy as you might think. It's not supported in Visual Web Developer Express 2008, and so it wouldn't work for my lab. In the full version of Visual Studio, we can strong name the assembly by adding a compiler option however this means that we need to create a new trust level. This is because modifying compiler options requires unmanaged code permissions. The suggestion is to use this modified trust level while debugging, then to deploy the assembly as a pre-compiled, strongly named assembly, with the unmodified trust level. I feel that this is probably a bad design choice – there should be a simple option for the Visual Studio built-in web server to perform strong naming on the ASP.NET code to support this type of scenario in medium trust. One note - Dominick points out that strong naming doesn't scale particularly well, since more than one strong name cannot be demand (this will result in an AND condition).
- Use the UrlIdentityPermission. I wasn't able to get this working, but theoretically you could restrict the caller's file location to the URL file://c:\directorytowebapp\*. Not sure if anyone has tried this successfully, but if you could, you may get around some of the difficulties around strong naming.
- Use a custom permission. This may be the most natural way to express the demand and justify the extra effort in creating a new permission, which isn't trivial.
Note that these permissions do not prevent Full Trust callers from accessing the assembly. See this blog post for a description of why "Full Trust Means Full Trust"
One major problem here is that the assembly will run in the GAC with Full Trust. This is a fairly significant privilege escalation, even for a very small piece of code. However, we can do better.
4. Partition the privileged code into an assembly and create a new custom trust level.
In this approach, we will combine approaches 2 and 3 to get the best of both worlds. We partition the code into a privileged assembly and the low privileged web site, taking into account the precautions discussed in the previous step. Then, rather than installing the code in the GAC, we create a custom policy file (as described in approach 2) and add a new code group which refers to our privileged assembly, and assign that code group a permission set that has only the requisite permissions. This approach is described in the hyperbolically-titled but otherwise helpful article "Never Write an Insecure ASP.NET Application Ever Again" and in Dominick's book (pages 330-332). One thing to note here is that you don't use the .NET Framework Configuration tool to set the security policy for the privileged assembly – you want to use the ASP.NET policies in the .config file. Boy, I wish I had known that a few days ago.
This approach gives us the best shot at least privilege, and is extremely flexible. However, it's probably not appropriate for the couple of hours that the students have to complete the lab. Hopefully in the future we'll get an easy way to a) secure a privileged assembly with a simple permission demand and b) create a new trust level based off medium trust and c) create a new code group in the custom trust level based on the strong name of the privileged assembly.