Customizing the .NET Common Language Runtime - Part 3
Customizing AppDomain Security
By, Dan Buskirk
September, 2007

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.

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".

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 the author:
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.