An Enhanced Version of the Sandboxed AppDomain

Last week I showed how to create an AppDomain with a limited set of permissions.  I also presented an easy way to create a StrongNameMembershipCondition.  Now I'll put the two together to make an enhanced version of the sandboxed AppDomain.

Why create a new version?   The version I presented last time works wonderfully if you're going to create the AppDomain and then call AppDomain.ExecuteAssembly to run the untrusted code in it.  However, especially in plugin scenarios, a more common approach is to create a MarshalByRefObject in your application that you load into the new AppDomain, and then have this proxy object invoke the untrusted assembly.  This scenario won't work with the last version of CreateRestrictedDomain, since all code loaded into the AppDomain will be granted the same limited set of permissions.

This can be easily rectified by modifying CreateRestrictedDomain to take a second parameter, which is an extra code group to add to the policy:

/// <summary>
/// Create an AppDomain that contains policy restricting code to execute
/// with only the permissions granted by a named permission set
/// </summary>
/// <param name="permissionSetName">name of the permission set to restrict to</param>
/// <param name="extraCodeGroup">extra code groups to add</param>
/// <exception cref="ArgumentNullException">
/// if <paramref name="permissionSetName"/> is null
/// </exception>
/// <exception cref="ArgumentOutOfRangeException">
/// if <paramref name="permissionSetName"/> is empty
/// </exception>
/// <returns>AppDomain with a restricted security policy</returns>
public static AppDomain CreateRestrictedDomain(string permissionSetName, CodeGroup extraCodeGroup)
{
    if(permissionSetName == null)
        throw new ArgumentNullException("permissionSetName");
    if(permissionSetName.Length == 0)
        throw new ArgumentOutOfRangeException("permissionSetName", permissionSetName, "Cannot have an empty permission set name");
        
    // Default to all code getting nothing
    PolicyStatement emptyPolicy = new PolicyStatement(new PermissionSet(PermissionState.None));
    UnionCodeGroup policyRoot = new UnionCodeGroup(new AllMembershipCondition(), emptyPolicy);

    // Grant all code the named permission set passed in
    PolicyStatement permissions = new PolicyStatement(GetNamedPermissionSet(permissionSetName));
    policyRoot.AddChild(new UnionCodeGroup(new AllMembershipCondition(), permissions));
        
    // add the extra code groups to the tree
    if(extraCodeGroup != null)
        policyRoot.AddChild(extraCodeGroup);
        
    // create an AppDomain policy level for the policy tree
    PolicyLevel appDomainLevel = PolicyLevel.CreateAppDomainLevel();
    appDomainLevel.RootCodeGroup = policyRoot;

    // create an AppDomain where this policy will be in effect
    string domainName = String.Format("Restricted Domain: {0}", permissionSetName); 
    AppDomain restrictedDomain = AppDomain.CreateDomain(domainName);
    restrictedDomain.SetAppDomainPolicy(appDomainLevel);

    return restrictedDomain;
}

Now, in order to enable the MarshalByRefObject scenario, we just need to make a code group that grants FullTrust to the assembly that's creating the AppDomain.  This is easily done with the CreateStrongMembershipCondition method:

// create a code group that gives this assembly full trust
PolicyStatement fullTrust = new PolicyStatement(new PermissionSet(PermissionState.Unrestricted));
CodeGroup trustSelf = new UnionCodeGroup(CreateStrongNameMembershipCondition(), fullTrust);        
        
AppDomain restrictedDomain = CreateRestrictedDomain("Internet", trustSelf);

Using the above snippet creates an AppDomain policy similar to:

  • AllCode: Nothing
    • All Code: Internet
    • AppStrongName: FullTrust

As you can see, this policy will grant Internet permissions to all code in the AppDomain except for the assembly that created the domain, which will remain fully trusted.