Code Access Security (CAS)

Silan Liu

 

1.    Overview. 1

2.    Fundamentals. 1

3.    Demand & Assert 3

3.1.     Scenario to use both Demand & Assert 3

3.2.     Another Scenario to use Assert 5

4.    Deny & PermitOnly. 6

5.    Imperative & Declarative Security. 6

6.    Conclusion. 7

 

1.      Overview

This article does not fully cover all Code Access Security (CAS) issues. Such kind of information can easily be found in MSDN and other articles. Instead this article tries to provide some insight on where CAS can be used and how to make it work, from a practical point of view, using scenarios and step-by-step sample code in Visual Studio .NET. It points out many practical pitfalls/tricks that are not or poorly documented in MSDN.

2.      Fundamentals

“Code Access Security allows code to be trusted to varying degrees, depending on where the code originates and on other aspects of the code's identity” [MSDN]. The way for a user to assign permissions to a particular assembly or a category of assemblies is to create (under the OS) a permission set which contains all needed permissions, and create a code group which associates this permission set with the assembly. If the code wants to do things that are not permitted, a SecurityException will be thrown.

To configure Code Access Security, go to “Control Panel” | “Administrative Tools” | “Microsoft .NET Framework Configuration”. There are three levels of runtime security policies: “Enterprise”, “Machine” and “User”, whose names explain themselves. If you log in as user, you will not be able to modify the first two, but only the user policy level.

By default, under “user” policy level, there is a code group called “All_Code”. This applies to all assemblies that are not specificlly associated with any other code groups. This “All_Code” code group is by default associated with “FullTrust” permission set, which means that all assemblies that you haven’t imposed any specific security control will have full trust. This is obviously not what a user wants if he wants a tight security on his computer. The associated permission set of the “All_Code” code group can be changed.

Now, as a demonstration, create two assemblies both with strong names: Assembly1 is used to write a file into “c:\Any”, while Assembly2 simply invokes Assembly1:

[Assembly1]

using System;

using System.Security;

using System.IO;

 

//[assembly: AllowPartiallyTrustedCallers()]

 

namespace Assembly1

{ 

   public class Doer

   {

     public void DoJob()

     {

        StreamWriter sw = File.CreateText("c:\\Any\\test.txt");

        sw.WriteLine("Hello world!");

        sw.Close();

     }

   }

}

[Assembly2]

using System;

using Assembly1;

 

namespace Assembly2

{

   class Class1

   {

     [STAThread]

     static void Main(string[] args)

     {

        Doer doer = new Doer ();

        doer. DoJob ();

     }

   }

}

Now follow the following steps. Make sure that you log in as a user not an administrator.

1.    Run the solution. The file should be successfully written. This is because the assemblies has not been associated with any code group, and therefore belongs to the “All_Code” code group with FullTrust permission set by default.

2.    Now create two permission sets, both with permission to write into “c:\Any”. Create two code groups to associate the two permission sets with corresponding assemblies. When run, a SecurityException will be thrown.This is because an assembly with strong name can only be called by a caller with FullTrust permission set.

3.    Now uncomment the attribute AllowPartiallyTrustedCallers in Assembly1. The solution should run OK.

4.    Now deprive the write permission of Assembly1 and run it again. A SecurityException will be thrown in Assembly1 at the point where the StreamWriter is created. If you deprive the write permission of Assembly2 instead of Assembly1, the same thing will happen. This is because, by default, at the point of accessing a controlled resource, the runtime security triggers a stack walk to make sure that the method or assembly and all its upstream callers have the needed permissions.

5.    Now assign permission to both assemblies to write to “C:\”, and correspondingly, change the code in Assembly1 to write to “c:\”. Run the solution. An UnauthorizedAccessException will be thrown. This is because a user is not allowed to write into “c:\”. Therefore even if the security policy allows an assembly to write into it, the runtime security still does not allow it.

Here is a summary of some facts or rules:

1.    If you want to restrict the permissions given to an assembly to only those contained in the associated permission set, you must tick the code group option “The policy level will only have the permissions from the permission set associated with this code group”. Otherwise what is granted to the assembly is the permissions of the particular assocated permission set plus permissions of the associated permission set of the inherited code group (“All_Code” group).

2.    All assemblies must be given “Enable assembly execution” security permission so that it can be run or launched.

3.    Permissions included in an assembly’s associated permission set that are above the logged-in user’s previllege will not be granted.

4.    A strongly named assembly can only be called by a fully-trusted caller, unless this assembly states AllowPartiallyTrustedCallers. When you use this attribute, it means that you have fully reviewed your code and there is no security flaw that may be used by luring attackers – such as a improperly used Assert. Not all system assemblies are marked with this attribute. You can look at the assembly’s manifest to see whether it has that attribute.

5.    However, an assembly belonging to the root “All_Code” code group can be called by partially-trusted callers, even if they are strongly named. This is probably because, if you don’t impose a particular security control on an assembly, the runtime security thinks that this assembly is not extremely critical.

6.    When you states AllowPartiallyTrustedCallers in an assembly, or let it stay in the “All_Code” code group, a permission-checking stack walk is still going to be triggered for every attempt to access any controlled resource. The only difference is if you improperly make a Assert you will make luring attacks possible.

3.      Demand & Assert

Demand and Assert are the two most important features of CAS. After you understand why they are there, you have no problem to understand all other CAS concepts.

As discussed before, by default an upstream security stack walk is triggered automatically when the code tries to access a controlled resource. To trigger such a stack walk manually before accessing the resource, you can use CodeAccessPermission.Demand. Just create a permission with all permissions that you want to check, then call its Demand method.

On the other hand, if you want to disable the upstream stack walk when a controlled resource is accessed, use CodeAccessPermission.Assert. Create a permission with all permissions that you want to avoid checking, and call its Assert method.

3.1.    Scenario to use both Demand & Assert

Suppose:

1.    The following call stack happens:

A=> B => C => D => E => F 

2.    Procedure C, D, E, F are in the same assembly written by you, and procedure A and B are future callers of your code.

3.    In procedure F there is a line of code to write to local disk.

4.    From beginning of C to the disk access line in F, there are lots of time and resources spent.

By default, only until the program runs to the disk access line in F, will the runtime security start the stack walk from F to A, to check whether each caller has the disk access permission. If any caller does not have the permission, a SecurityException will be thrown at that disk access line. By this time, a lot of time and resources have been wasted.

Because procedure C, D, E and F are all under your control, you know that once given the disk access permission, procedure C, D, E and F do not deny it. So what you really need to do is only to find out at the beginning of procedure C whether your assembly and the upstream callers such as A and B have the permission. If they don’t, the call stack fails at the beginning of C, avoiding all the time and resource waste in C, D, E and F.

Moreover, if they have that permission, when the program runs to the disk access line in F, you may want to disable the automatic upstream stack walk.

The following sample code involves three strongly-named assemblies. Assembly3 calls Assembly2, who calls Assembly1, who writes to the local disk:

Assembly3 => Assembly2 => Assembly1

The Demand and Assert code are initially commented out.

Now follow the following experimenting steps:

1.    Create three permission sets which are associated with the three assemblies separately, all with permission to write to “d:\Any”. Run the solution. The solution should be OK.

2.    Deprive the permission from Assembly3. Run the solution. A SecurityException will be thrown at the disk access line in Assembly1.

3.    Add Security permission “Assert any permission that has been granted” to Assembly1 and uncomment the Assert code in Assembly1. The solution should run OK.

4.    Uncomment the Demand code in Assembly2. A SecurityException will be thrown at the Demand point.

5.    Give the disk access permission back to Assembly3. The solution should run OK.

[Assembly1]

using System;

using System.Security;

using System.Security.Permissions;

using System.IO;

using System.Windows.Forms;

 

 

[assembly: AllowPartiallyTrustedCallers()]

 

namespace Assembly1

{ 

   public class Doer

   {

     public void DoJobs()

     {

        //FileIOPermission f = new FileIOPermission(FileIOPermissionAccess.Write |

          //FileIOPermissionAccess.Read, "d:\\Any");

        //f.Assert();

 

        StreamWriter sw = File.CreateText("d:\\Any\\Assembly1.txt");

        sw.WriteLine("Hello world!");

        sw.Close();

     }

   }

}

[Assembly2]

using System;

using System.Security;

using System.Security.Permissions;

using Assembly1;

 

[assembly: AllowPartiallyTrustedCallers()]

 

namespace Assembly2

{

   public class FirstCaller

   {

     public void PassOn()

     {

        //FileIOPermission f = new FileIOPermission(FileIOPermissionAccess.Write |

          //FileIOPermissionAccess.Read, "d:\\Any");

        //f.Demand();

 

        Doer doer = new Doer();

        doer.DoJobs();

     }

   }

}

[Assembly3]

using System;

using Assembly2;

 

namespace Assembly3

{

   class SecondCaller

   {

     [STAThread]

     static void Main(string[] args)

     {

        FirstCaller fc = new FirstCaller();

        fc.PassOn();

     }

   }

}

3.1.    Another Scenario to use Assert

Using the same A => F call stack example. Suppose all that procedure F has to do with disk access is to write a log file into a safe place like “C:\Temp”, and the file is generated locally by F and never read back by any application. In this case you know that callers of procedure F can not use F to do any damage. In this case you may want to disable the upstream stack-walk for disk access permission triggered from F by putting Assert in F before its disk access code. Stack walk itself is a serious overhead which should be avoided if it is not needed.

Note that I put “triggered from F” in the previous sentence on bold font, because if a caller up in the stack such as E also contains disk access code, it will still trigger stack walk. Only the code after the asserting point in the same method in procedure F stops triggering stack walk.

Two things to note about Assert:

1.    In order to make an Assert, the entity should be given an assert-making security permission by ticking the “Assert any permission that has been granted” option in its security permission;

2.    The entity which makes the assert should have the asserted permissions itself.

4.      Deny & PermitOnly

If a method creates a permission like what we have been doing before, and call its Deny method, and if any operation (including a call to Demand) downstream in the call stack triggers a stack walk for a permission in the specified permission set, that security check will fail when it reaches the Deny, and a SecurityException will be thrown at the triggering operation.

If you do not want your code to accidentally do some damage, you can put a call to Deny at the entrance of your assembly, so that all subsequent code are denied of certain permissions.

If a method calls PermitOnly on a set of permissions, and if any operation (including a call to Demand) downstream in the call stack triggers a stack walk for a permission not in the specified set, that security check will fail when it reaches the PermitOnly, and a SecurityException will be thrown at the triggering operation.

PermitOnly is used in similar scenarios as Deny.

5.      Imperative & Declarative Security

All security measures discussed above are done through imperative security measures. Another type is declarative security, in which you put security-related attributes at method, class or assembly level.

Attribute SecurityAction.Demand functions similarly as imperative Demand. This attribute can only be put at class or method level, not at assembly level. It triggers a stack walk before the class is created (if class level) or method called (if method level).

Attribute SecurityAction.RequestMinimum can only be put at assembly level. It tells the runtime: “If the following permissions are not given to this assembly, don’t load it, and throw a PolicyException”. Note: the runtime only looks at the permissions given to the assembly itself. It does not perform a stack walk to check the permissions of upstream callers.

SecurityAction.RequestOptional tells the runtime: “It is good to grant the following permission(s) to this assembly. But the assembly should be loaded even if this permission is not granted.”

SecurityAction.RequestRefuse tells the runtime: “Even if the security policy gives the following permission(s) to this assembly, please act as if they are not given, i.e., throw a SecurityException if relevant resource is accessed”.

In the following example, although method DoJob is empty, if this assembly does not have the requested permission, the loading of this assembly will still fail.

using System;

using System.Security;

using System.Security.Permissions;

 

[assembly: AllowPartiallyTrustedCallers()]

[assembly: UIPermission(SecurityAction.RequestMinimum,

   Window = UIPermissionWindow.AllWindows)]

 

namespace Assembly1

{ 

   public class Doer

   {

     public void DoJobs()

     {}

   }

}

To compare imperative and declarative security:

1.    Using imperative security, the user can not find out from your assembly itself what kind of permission is required or not wanted. But if you use declarative security, the request is contained in the assembly’s metadata, which can be viewd by ildasm.exe or permview.exe This can help the administrator of user’s computer to setup security policy.

2.    Using declarative attributes, in most cases, it is “staticly” decided whether an assembly is to be loaded or not, class to be created or not, and method to be called or not. While if using imperative command, all these decisions are made at run time. Your code can even say: “If the following condition is true than I do not want to do a stack walk…”. So imperative commands are more flexible.

6.      Conclusion

.NET code access security is not some enhancing function you can choose to adopt or not. Even if you don’t know anything about CAS when you write a .NET application, your application is still subjected to whatever security scrutiny that the user’s computer wants to impose. Knowing the tricks of CAS can help you better utilize its functionality and make your code more efficient.