Customizing the .NET Common Language Runtime - Part 3

Posted by Dan Buskirk on 09/1/07 | DotNet

 In our previous code, we created a custom AppDomain manager which demonstrated that it was loaded by the CLR, but otherwise did nothing. We will now turn our attention to creating our first practical AppDomain manager. We wish to ensure that any code compiled in our small test application cannot do any damage to the system. One way to achieve this goal is to establish security limitations on the new AppDomain into which this code is loaded and executed.

Rather than clutter the AppDomainManager class from Part 2 with more code, we will create a new class, called adManager, from which we have stripped some of the reporting code and the comments. The previous AppDomainManager implemented a HostSecurityManager property which returned an instance of the .NET HostSecurityManager class. With a minor modification, we now return an instance of a new class, CustomHostSecurityManager. Here's the code:

    1   using System;
    2     using System.Reflection;
    3     using System.Runtime.Hosting;
    4     using System.Security;
    5     using System.Security.Policy;
    6     using System.Threading;
    7 
    8 namespace AppDomainManager
    9 {
   10 
   11         public sealed class adManager : System.AppDomainManager
   12         {
   13             HostSecurityManager _HostSecurityManager = null;
   14             // (1)
   15             // Only fully-trusted code can be loaded in an AppDomainManager constructor
   16             public adManager()
   17                 : base()
   18             {
   19                 return;
   20             }
   21 
   22             // (2) In this example, we don't do any work, just pass the request to the base class
   23             public override AppDomain CreateDomain(string friendlyName, Evidence securityInfo, AppDomainSetup appDomainInfo)
   24             {
   25                 return base.CreateDomain(friendlyName, securityInfo, appDomainInfo);
   26             }
   27 
   28             // (3) A CLR host can provide a custom security manager to fine-tune the
   29             // security behavior of an AppDomain. This propery provides programmatic access
   30             // to a HostSecurityManager. In this example, we return a reference
   31             // to a new CustomHostSecurityManager object.
   32             public override HostSecurityManager HostSecurityManager
   33             {
   34                 get
   35                 {
   36                     if(_HostSecurityManager==null)
   37                         _HostSecurityManager= new CustomHostSecurityManager();
   38                     return _HostSecurityManager;
   39                 }
   40             }
   41 
   42         }
   43     }
   44 
   45 
   46 

Figure 1.

Now let's take a look at a HostSecurityManager. In this example, we are going to ensure that code run in a new AppDomain runs under a restricted permission set. There are many options for restricting security in a HostSecurityManager. By default, .NET code loaded from the local machine has full trust. Obviously, there will be circumstances where this full trust is not appropriate. Soon, we will be creating a new permission set called "Barney", or whatever you like.  Our CustomHostSecurityManager will create a DomainPolicy object based on the permission set defined as Barney in the .NET machine configuration.

Let's take a look at the code:

    1 using System;
    2 using System.Collections;
    3 using System.Net;
    4 using System.Reflection;
    5 using System.Security;
    6 using System.Security.Permissions;
    7 using System.Security.Policy;
    8 using System.Security.Principal;
    9 using System.Threading;
   10 using System.Runtime.InteropServices;
   11 using System.Runtime.Hosting;
   12 
   13 
   14 namespace AppDomainManager
   15 {
   16 
   17     [Serializable()]
   18     //(1) Make sure we have the necessary permission to connect with the CLR
   19     [SecurityPermissionAttribute(SecurityAction.Demand, Flags = SecurityPermissionFlag.Infrastructure)]
   20     public class CustomHostSecurityManager : HostSecurityManager
   21     {
   22         public CustomHostSecurityManager()
   23         {
   24             Console.WriteLine(" Constructing CustomHostSecurityManager.");
   25         }
   26 
   27         private PolicyLevel _DomainPolicy = null;
   28         // (2) We'll make use of an existing .NET permission set
   29         private static NamedPermissionSet _NamedPermissionSet;
   30 
   31         private HostSecurityManagerOptions hostFlags = HostSecurityManagerOptions.HostPolicyLevel;
   32 
   33         //(3)
   34         //DomainPolicy is called by the CLR every time an
   35         // assembly is loaded. Therefore DomainPolicy itself
   36         // cannot load any assemblies or infinite recursion will result
   37         public override PolicyLevel DomainPolicy
   38         {
   39             get
   40             {
   41                 //(4)
   42                 //Make sure we don't apply inappropriate restrictions to the default domain
   43                 if (AppDomain.CurrentDomain.FriendlyName == "DefaultDomain" ||
   44                     AppDomain.CurrentDomain.FriendlyName == "SomeOtherName")
   45                     return null;
   46                 //(5)
   47                 // only need to create an AppDomainPolicy if it has not yet been done
   48                 if (_DomainPolicy == null)
   49                     _DomainPolicy = CreateAppDomainPolicy();
   50                 return _DomainPolicy;
   51             }
   52         }
   53 
   54 
   55         public override HostSecurityManagerOptions Flags
   56         {
   57             get
   58             {
   59                 //Console.WriteLine("hostFlags returned");
   60                 return hostFlags;
   61             }
   62         }
   63 
   64 
   65 
   66         //(6)
   67         private static PolicyLevel CreateAppDomainPolicy()
   68         {
   69             Console.WriteLine("CreateAppDomainPolicy called.");
   70             PolicyLevel pLevel = PolicyLevel.CreateAppDomainLevel();
   71             // (7) We can create a code group with permissions to union
   72             // with the defined permission set, if we like.
   73             // Here, we give the root code group no permissions
   74             UnionCodeGroup rootCodeGroup;
   75             PermissionSet ps = new PermissionSet(PermissionState.None);
   76             ps.AddPermission(new SecurityPermission(SecurityPermissionFlag.NoFlags));
   77             rootCodeGroup = new UnionCodeGroup(new AllMembershipCondition(),
   78                 new PolicyStatement(ps, PolicyStatementAttribute.Nothing ));
   79 
   80             FindNamedPermissionSet("Barney");
   81             UnionCodeGroup MyCodeGroup = new UnionCodeGroup(
   82                 new ZoneMembershipCondition(SecurityZone.MyComputer),
   83                 new PolicyStatement(_NamedPermissionSet,
   84                 PolicyStatementAttribute.Nothing));
   85             MyCodeGroup.Name = "Basic Intranet Stuff";
   86             // Add the code groups to the policy level.
   87             rootCodeGroup.AddChild(MyCodeGroup);
   88             pLevel.RootCodeGroup = rootCodeGroup;
   89             return pLevel;
   90         }
   91         //(8)
   92         private static void FindNamedPermissionSet(string name)
   93         {
   94             IEnumerator policyEnumerator = SecurityManager.PolicyHierarchy();
   95 
   96             while (policyEnumerator.MoveNext())
   97             {
   98                 PolicyLevel currentLevel = (PolicyLevel)policyEnumerator.Current;
   99 
  100                 if (currentLevel.Label == "Machine")
  101                 {
  102                     IList namedPermissions = currentLevel.NamedPermissionSets;
  103                     IEnumerator namedPermission = namedPermissions.GetEnumerator();
  104 
  105                     while (namedPermission.MoveNext())
  106                     {
  107                         if (((NamedPermissionSet)namedPermission.Current).Name == name)
  108                         {
  109                             Console.WriteLine("Named permission set " +
  110                                 ((NamedPermissionSet)namedPermission.Current).Name + " found.");
  111                             // Save the LocalIntranet permissions set for later use.
  112                             _NamedPermissionSet = ((NamedPermissionSet)namedPermission.Current);
  113                         }
  114                     }
  115                 }
  116             }
  117         }
  118     }
  119 
  120 }
  121 
  122 

Figure 2.

Comment (1) identifies an example of declarative code access security in .NET. This code demands to be given the Infrastructure permission; in other words, it is telling the CLR that if it cannot be given permission to interact with the CLR, it should not be loaded and an exception should be thrown. While good practice, our CustomHostSecurityManager does not strictly speaking require this. Comment (2) marks the declaration of a variable of type NamedPermissionSet. We will have to obtain this permission set from .NET itself and will stash a reference here once we do. We are now ready to lay the groundwork for our DomainPolicy. As comment (3) notes, it is critical not to include any code in the DomainPolicy property which would result in loading an assembly. The CLR will call our policy property every time an assembly is loaded. Any assembly loading instigated by the DomainPolicy property itself will result in a recursive disaster.

We only want to impose our custom security policy on some AppDomains. Recall that our custom AppDomainManager will be loaded into all AppDomains, including the default one. We could create impossible code by putting incorrect restrictions on the default AppDomain. Therefore, we will test to see which AppDomain is running the code in our CustomHostSecurityManager object. We test for the name "DefaultDomain", as is marked by comment (4). We also show how to test for "SomeOtherName", but this is only for illustrative purposes and does not have any functional consequence for our code. If, however, our code is being executed by any other AppDomain, we return a reference to our DomainPolicy object. Of course, the first time the property is called, we will have to create this object.

It is the CreateAppDomainPolicy method, at comment (6) which does most of the heavy lifting in this class. At comment (7), we create a new code group to illustrate how this is done, but by creating the permission set with SecurityPermissionFlag.NoFlags, we essentially give this new group no permissions. The necessary permissions will have to come from our configuration permission set "Barney". The FindNamedPermissionSet method at (8) cruises through the list of permission sets defined in the machine configuration, which we will explore shortly. If the desired permission set is found, it is stashed in the variable _NamedPermissionSet.

A minor modification will have to be made to our test platform. The original code created to compile C# code inherited from the MarshalByRefObject class. This class demands full trust from the CLR. Since we are going to limit the trust given to all code running in our AppDomain, we will have to remove the inheritance and supply a serializable attribute instead. We recall form part 2 that this is required so that code in the new AppDomain can be invoked from code in the default AppDomain.

   13 [Serializable]
   14 public class CompileInvoke //: MarshalByRefObject
   15 {
   16 
   17     public void RunIt(string CodeSnippet)
   18     {

Figure 3.

Full source is provided in the zip file.

All that remains is to create a permission set for to be used by our AppDomain. The easiest way to do this is by using the gui configuration tool supplied with the Visual Studio. The file mscorcfg.msc can be found in your Visual Studio directory tree. The default location is C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin. Double-click on this file and then drill down to the Permission Sets folder of the machine folder in Runtime Security Policy.

.NET Customizing CLR

Figure 4.

Right-click on the Permission Sets folder and choose "New". Enter Barney for the new name. You will be prompted to add permissions to the new empty set. You may wish not to add any, just to see if the code execution fails. If you choose not to add permissions now, you can add them later by clicking on Barney and choosing "Change Permissions". The Create Permission dialog is shown in Figure 5. You will want to add "Reflection" and grant the permission "Reflection Emit", since that is how we create our compiled code in our project. You will then want to add "Security" and grant "Enable Assembly Execution".

.NET Customizing CLR

Figure 5.

Your script code should now execute under the new security policy in your custom application domain.

In this example, we have seen how to use a custom HostSecurityManager class to apply policy and permissions. This technique determines the security for the entire AppDomain at once. Applications can, of course, apply code access security at any level required. In our present application, we might apply code access security to the method which runs the user's script code. This would have the advantage of applying security soley to the user's script code without putting unnecessary restrictions on the full AppDomain. The only thing that remains now in our CLR customization example is to provide the unmanaged host code with the necessary information to interact with our custom managers as necessary. We shall tackle that in the next installment.

Source Code

The source code for all of the examples above can be found here

About Dan Buskirk:

 Dan Buskirk had been a research scientist for many years when he left the university labs to participate in a startup venture to design and build databases for medical science. Since that time he has managed a consulting practice specializing in database design and development. Dan balances this with training on Microsoft SQL Server, Microsoft Analysis Services, and .NET programming. His interests include mathematical methods for data mining and computing methods for advanced data categorization on Windows clusters.



Want to learn more about .NET Training?

0 Comments

COMMENTS

Name:
URL:
Comment:

Comments are disabled for this article.