Security of Dynamically Compiled Assembly
While working on the Dynamic Compilation in C# project, I stumbled into an interesting problem: how do you set up security on a newly compiled assembly? If the code we are compiling is some kind of script that comes from the users, we may want to run it in a sandboxed environment.
Amazingly, there is no direct way to instruct .NET to "load this assembly with these permissions". What can or cannot be done by a particular piece of code is determined indirectly, by a complex set of rules. There are two main factors that determine currently active permissions:
- Properties of the executing assembly, a.k.a. "evidence": it's name, has, strong key, URL
- Permissions of the code up the call stack
CLR translates assembly evidence to actual permission set by using policies. A policy is a set of statements like "assemblies signed by cryptographic key X get permission set Y". There are several levels of policies: enterprise, machine, user, and appdomain. The appdomain level policy is easy to set programmatically, but it cannot be changed once the domain is running.
The prescribed way to sandbox untrusted code is to create an appdomain and tweak its security policy, e.g. as described in "The Simple Sandboxing API". This, however, is a problem if we want the compiled code to share as much context as possible with the calling code.
Alternative way to limit assembly permissions is to use
RequestRefuse
and RequestOptional
attributes
as shown in "Assembly Level Declarative Security".
This, however, can be done only
from within the assembly and only declaratively: as far as I know, there is no API
to limit the assembly permissions prior to loading. Since we compile the assembly,
we could put relevant attributes there, but nothing prevents the user from putting
additional [RequestOptional]
entries in his code, thus defeating
the purpose of the sandbox.
Another way to limit permissions is to call SomePermission.Deny()
,
or SomePermission.PermitOnly()
. This sounds like a good idea,
but it is not, since the compiled code can revert the Deny()
by
calling Assert()
.
See "Deny and PermitOnly Are Not For Sandboxing"
for more details.
They would not be able to do it, however, if their assembly is marked with
[SecurityTransparent]
attribute. Such assembly
cannot elevate permissions.
Also, once we put this attribute in, it cannot be removed or reverted by adding more code.
Thus, this is the strategy I chose for my compiled code: I call PermitOnly()
on the intended permission set and mark the compiled assembly with the [SecurityTransparent]
attribute. The tests show that this approach works, and I only hope that there is no some
clever security hack around it.