Assembly Level Declarative Security

Assembly level declarative security comes in three forms, RequestMinimum, RequestOptional, and RequestRefuse.  The three can be briefly defined as:

  • RequestMinimum -- the set of permissions that are absolutely required for this assembly to run
  • RequestOptional -- the set of permissions that would be nice to run with, but are not required
  • RequestRefuse -- a set of permissions that should never be granted to the assembly

Each of these actions can be applied on multiple attributes in each assembly, the final set of permissions in the action will be the union of all of the attributes with the given action.

Most people intuitively understand RequestRefuse.  Any permissions that are in this set are added to the assembly's refused set, and the assembly will never execute with these permissions granted.  The other two can become a bit dicey however, especially in their side effects.  In order to demonstrate RequestMinimum and RequestOptional, I'll use the following sample code, and apply different assembly-level declarative security to it.

public static class DeclarativeSecurity
{
    public static void Main()
    {
        Console.WriteLine("In Main");
        if(HaveFileIO())
            Console.WriteLine("Have full FileIO permission");
        else
            Console.WriteLine("Do not have full FileIO permission");
    }

    private static bool HaveFileIO()
    {
        try
        {
            new FileIOPermission(PermissionState.Unrestricted).Demand();
        }
        catch(SecurityException)
        {
            return false;
        }

        return true;
    }
}

RequestMinimum tells the CLR that your code cannot run without the given set of permissions.  So, if I were to attach the following attribute to my assembly:

[assembly: FileIOPermission(SecurityAction.RequestMinimum, Unrestricted = true)]

And run it from the Intranet zone, the code would never execute.  Since the CLR sees that this assembly cannot run without unrestricted FileIO permission, and the Intranet permission set won't grant it that permission set, the loader refuses to even load the assembly.  Instead, you'll see a FileLoadException with an inner PolicyException:

Unhandled Exception: System.IO.FileLoadException: Could not load file or assembly 'ReqMin, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of itsdependencies. HRESULT 0x80131417 (CORSEC_E_MIN_GRANT_FAIL) Failed to grant minimum permission requests.
File name: 'ReqMin, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' --->
System.Security.Policy.PolicyException: Required permissions cannot be acquired.

at System.Security.SecurityManager.ResolvePolicy(Evidence evidence, PermissionSet reqdPset, PermissionSet optPset, PermissionSet denyPset, PermissionSet& denied, Boolean checkExecutionPermission)
at System.Security.SecurityManager.ResolvePolicy(Evidence evidence, PermissionSet reqdPset, PermissionSet optPset, PermissionSet denyPset, PermissionSet& denied, Int32& grantedIsUnrestricted, Boolean checkExecutionPermission)

RequestOptional has a different effect.  Changing the security attribute to:

[assembly: FileIOPermission(SecurityAction.RequestOptional, Unrestricted = true)]

And running from the Intranet zone, I'll now see:

In Main
Do not have full FileIO permission

So the application loaded and ran, but was not granted FileIO permission.  This is what I would expect, since Intranet assemblies are not granted unrestricted FileIO, however the RequestOptional action told the CLR that my code would still be able to run without these permissions.  Generally code that works with RequestOptional tends to try a privileged operation in a try ... catch(SecurityException) block, and fall back to alternate behavior if the optional permissions are not granted.

So far, so good.  However, now we come to the interesting side effect, one that oftentimes confuses people new to assembly level declarative security.  Namely, using any declarative security at all has the potential to decrease the permission set that your assembly is running with.  This is obvious in the case of RequestRefuse, however with RequestMinimum and RequestOptional, the reasoning is far less clear.

Internally, the CLR takes the union of the RequestMinimum and RequestOptional sets (and adds to that execution permission), and that resulting set is the only set of permissions that your application will have when it's running.  For instance, demanding unrestricted Registry permission will fail if either of the above declarative requests are in place.

The reasoning behind this is that the CLR takes RequestMinimum and RequestOptional to be the complete set of permissions that your assembly requires to run.  If you think about what you're saying to the runtime, this makes a lot of sense.  In effect you're saying "here's the minimum set of permissions I require to run, but I'll also provide enhanced behavior with these other permissions."  The combination of those sets should result in every permission your assembly requires.  Limiting the set of permissions that an assembly is running with increases the security of your application, since there's now no way that your code can access resources that it doesn't need access to.  In the above example, there would be no way for an attacker to trick my code into, say, accessing the registry.

Because of the above, its often better to over-estimate the permissions you need (ask for Unrestricted FileIO rather than just FileIO to a specific path), since if you under-estimate users of your application will run into SecurityExceptions.  In fact, the FileIO case mentioned above is one instance where you often do have to over-estimate, because generally your application won't know what the full path to the file it wants to work with is until runtime.  Additionally, it's important to note that these restrictions are per-assembly.  The only way that the restricted grant to this assembly could affect other assemblies in my application is if a method from this assembly happened to be on a call stack when a demand took place.

As if this wasn't getting confusing enough already, there's one more sticky point that can trap you.  It's important to realize that the defaults for the various permission sets are:

  • RequestMinimum -- Nothing
  • RequestOptional -- FullTrust
  • RequestRefuse -- Nothing

Since the permission set restriction is calculated by taking the minimum request and unioning with the optional request, and the default optional request is FullTrust, unless an Optional request is made, the union of the minimum request and the FullTrust request will result in a FullTrust set.  What this boils down to is that unless you specify a RequestOptional set, the permissions your application runs under will not be limited in the fashion stated above.

A common question that arises after people start to understand that they don't get any more permissions than they specifically ask for is what use is RequestRefuse?  It seems silly for me to RequestRefuse RegistryPermission if I do a RequestOptional on FileIO permission since the refusal will have no net effect.  Generally, RequestRefuse is there for the times when it's easier to say what permissions you don't need than it is to specifically call out the permissions that you do need.

Update: Added information about defaults in order to clarify the example.