Increasing Permissions for Web-Deployed Windows Forms Applications

 

Chris Sells
Sells Brothers Consulting

November 5, 2002

Summary: Chris Sells discusses permissions in .NET and how you can adapt the object model to protect smart clients while allowing well-known assemblies or sites to have additional permissions to provide users with additional services. (8 printed pages)

Download the winforms11122002.exe sample file.

After writing .NET Zero Deployment: Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients (a title that rolls right off the tongue, don't you think?), I have literally traveled the country speaking on this topic at conferences, user group meetings, and courses sharing the good news of smart client deployment. When I show folks how easy it is to put a Windows Forms executable into a virtual directory, surf to it using an URL and have it pop up outside of the browser with no installation, they're always stunned—for about three seconds. Then they recover and ask, "But there's no prompt dialog! How can that be secure?" At that point, I show them a sample that does something truly evil, like resetting your Mine Sweeper high scores, and launch that application from the same URL, where they are greeted with the unhandled exception dialog informing them that the application has attempted to violate the security policy and has been stopped in its tracks. I then go on to explain the limited permissions that .NET assemblies have when launched from the intranet or Internet. And this pleases them.

For about three seconds, when they ask, "But how do I write an application to do <something I just told them they couldn't do>?" Of course, developers want the deployment convenience of smart clients without the inconvenience of working around security violations. For example, the current .NET Framework doesn't allow an assembly from even the intranet zone to access the Active Directory® and as of Service Pack1 (SP1), an assembly from the Internet can't run at all, although both of these limitations happen for different reasons.

Groups of Code

Code in .NET is awarded permissions based on evidence, which typically means where the assembly originated. For example, if an assembly came from the intranet zone, it's awarded the permissions from the intranet permission set. You can view the permissions in the current permission sets on your machine using the Microsoft .NET Framework Configuration tool, available through the Administrative Tools menu in your Start menu. The permission sets on your machine are available under Runtime Security Policy | Machine | Permission Sets, as shown in Figure 1.

Figure 1. Default Named Permission Sets

Notice that the Internet permission set is selected in Figure 1 and lists a number of things that assemblies awarded the Internet permission set can do. However, the names of the permission sets are a little misleading, especially the Internet permission set. By default, as of SP1, assemblies from the Internet zone don't get the permissions from the Internet permission set; in fact, they get permissions from the Nothing permission set (which are none, as you might guess). The reason for this is that lists of permissions are separate from how permission sets are awarded. The mapping is provided by Code Groups.

A code group is how permission sets are awarded to assemblies. It combines a membership condition with a permission set. If the membership condition matches an assembly as it's loaded, baring some obscure exceptions, the assembly gets those permissions. The default code groups are shown in Figure 2.

Figure 2. Default .NET Code Groups

For example, under SP0, the Internet_Zone code group gave all assemblies from the Internet zone permissions from the Internet permission set. As of SP1, the Internet_Zone code group awards all assemblies from the Internet zone permissions from the Nothing permission set. So, the reason that assemblies from the Internet can't run as of SP1 is that they don't have any permissions at all, including the Security.Execute permission.

Awarding permissions to your own assemblies is a matter of creating a code group with a membership condition that matches your assemblies. For instance, what site it came from, what URL it came from, what key it's signed with, and so on, and matching it to a set of permissions. Further, you can decide to build your own custom permission set or you can use one of the default permission sets. For example, if you're deploying from the intranet and need access to the Active Directory services, you may decide to create a custom permission set, add the permissions you need, including Directory Service permissions, and deploy that to your client machines (more on deploying permissions later). The problem, however, is that still won't do the trick.

Allowing Partially Trusted Callers

Signing your .NET assemblies is good practice and highly recommended because signed assemblies have some special properties. For example, only signed assemblies get version checking. Also, only signed assemblies get matched by the loading client to see if the assembly being loaded is the one that it was built against. And, for the purposes of this discussion, signed assemblies get greater protection against partially trusted code, which is any code that is not assigned the FullTrust permission set. The "greater protection" that a signed assembly gets is that it may not be called by any partially trusted code. As an example, System.DirectoryServices.dll is the part of the .NET Framework class libraries where access to the Active Directory is implemented. Being part of the .NET Framework, System.DirectoryServices.dll is signed and therefore, any of your code that would be launched through a URL using the default code group may not access it. In other words, you can award your assemblies permission to get to Directory Services all day long, but unless they're awarded the FullTrust permission set, they can't access the Active Directory.

System.DirectoryServices.dll is not the only such assembly. Many assemblies in .NET are protected from being used by partially trusted code. In fact, if you were to build your own DLL assembly and sign it, your assembly couldn't be used by your own EXE assemblies, even if they were downloaded from the exact same site and signed with the exact same key.

However, many assemblies from the .NET Framework can be used by your partially trusted code and you can provide access to your custom assemblies from a partially trusted assembly. The secret is the AllowPartiallyTrustedCaller attribute, which you can apply to your assembly in C# like so:

[assembly: AllowPartiallyTrustedCallers]

Be very careful when you do this, however, as now you're on the hook to make sure that your assembly is robust in the face of partially trusted callers. Microsoft was cautious in applying this attribute as can be seen in Table 1, which shows the major .NET assemblies that allow partially trusted callers as of .NET SP1.

Table 1. Major .NET assemblies and their APTC setting

Assembly Partially-Trusted Callers Allowed
Accessibility.dll yes
CustomMarshalers.dll no
Microsoft.JScript.dll no
Microsoft.VisualBasic.Compatibility.Data.dll no
Microsoft.VisualBasic.Compatibility.dll no
Microsoft.VisualBasic.dll yes
Microsoft.VisualBasic.Vsa.dll no
Microsoft.VisualC.Dll no
Microsoft.Vsa.dll no
mscorlib.dll yes
System.Configuration.Install.dll no
System.Data.dll yes
System.Design.dll no
System.DirectoryServices.dll no
System.dll yes
System.Drawing.Design.dll no
System.Drawing.dll yes
System.EnterpriseServices.dll no
System.Management.dll no
System.Messaging.dll no
System.Runtime.Remoting.dll no
System.Runtime.Serialization.Formatters.Soap.dll no
System.Security.dll no
System.ServiceProcess.dll no
System.Web.dll no
System.Web.RegularExpressions.dll no
System.Web.Services.dll yes
System.Windows.Forms.dll yes
System.XML.dll yes

If you need access to any assemblies not marked with the APTC attribute, you'll need to create a code group that awards your assembly the FullTrust permission set. None of the other default named permission sets, even the Everything permission set (which contains every permission defined by Microsoft), can award permissions to access assemblies not marked with APTC.

Awarding Permissions

Of course, as facile as you may become with the .NET permission policy administration tools, you'll never want to wander to each of your clients' machines to create the necessary code groups and permissions sets that they need to run your code, especially if that code is to be deployed across the Internet. Luckily, .NET provides classes to create code groups and permission sets. For example, the following code is used to create a custom code group to award all assemblies signed with a known strong name Internet permissions:

using System.Security;
using System.Security.Permissions;
using System.Security.Policy;

// Generated with 'secutil -c -s wahoo.exe'
byte[] publicKey = { 0, 36, ... };

// Find the machine policy level
PolicyLevel machinePolicyLevel = null;
System.Collections.IEnumerator ph = SecurityManager.PolicyHierarchy();

while( ph.MoveNext() ) {
  PolicyLevel pl = (PolicyLevel)ph.Current;
  if( pl.Label == "Machine" ) {
    machinePolicyLevel = pl;
    break;
  }
}

if( machinePolicyLevel == null ) return;

// Create a new code group giving Wahoo! Internet permissions
PermissionSet permSet1 = new NamedPermissionSet("Internet");
StrongNamePublicKeyBlob key = new StrongNamePublicKeyBlob(publicKey);
IMembershipCondition membership1 =
  new StrongNameMembershipCondition(key, null, null);

// Create the code group
PolicyStatement policy1 = new PolicyStatement(permSet1);
CodeGroup codeGroup1 = new UnionCodeGroup(membership1, policy1);
codeGroup1.Description = "Internet permissions for Sells Brothers Wahoo!";
codeGroup1.Name = "Sells Brothers Wahoo!";

// Add the code group
machinePolicyLevel.RootCodeGroup.AddChild(codeGroup1);

// Create a new code group giving all of sellsbrothers.com Execute permission
PermissionSet permSet2 = new NamedPermissionSet("Execution");
IMembershipCondition membership2 =
  new SiteMembershipCondition("www.sellsbrothers.com");

// Create the code group
PolicyStatement  policy2 = new PolicyStatement(permSet2);
CodeGroup  codeGroup2 = new UnionCodeGroup(membership2, policy2);
codeGroup2.Description = "Minimal execute permissions for sellsbrothers.com";
codeGroup2.Name = "sellsbrothers.com minimal execute";

// Add the code group
machinePolicyLevel.RootCodeGroup.AddChild(codeGroup2);

// Save changes
SecurityManager.SavePolicy();

This code actually adds two code groups—one to award Internet permissions to all assemblies signed with a known strong name, and one to work around an issue in .NET SP1 where a site as a whole must have at least Execute permission for any other permissions awarded a strong name to take effect. We start by using the SecurityManager to find the top of the Machine runtime policy hierarchy, where we'll be adding new code groups. We then grab the Internet NamedPermissionSet and join it with a StrongNameMembershipCondition to produce the new code group, which we then name something that'll make sense in the administration tools, and add it to the root code group along with all of the existing code groups. If we had wanted to award our assemblies full trust, we'd have passed the string FullTrust instead of Internet when creating this NamedPermissionSet object. After creating the first permission set, we do the same thing again with the Execution NamedPermissionSet and the SiteMembershipCondition, naming it and adding it as well.

To commit the changes to the Machine runtime security policy, we ask the SecurityManager to save, and that's it. This code is all that's necessary to award permissions from existing permission sets to your assemblies. If you want to create custom permission sets, the code is similar, except you create instances of empty NamedPermissionSet objects and add permission objects that derive from CodeAccessPermission, like FileIOPermission and DirectoryServicesPermission. New permission sets are then added to the machine policy through the AddNamedPermissionSet method, like so:

// Create a named, empty permission set
NamedPermissionSet permSet =
  new NamedPermissionSet("My Permission Set", PermissionState.None);

// Add a permission
IPermission perm = new DirectoryServicesPermission();
permSet.AddPermission(perm);
machinePolicyLevel.AddNamedPermissionSet(permSet);

Notice the use of PermissionState.None when passed to the NamedPermissionSet constructor. Without that, you get a permission set just like FullTrust, with all permissions. Instead, you want an empty permission set that only has permissions that you explicitly add.

Deploying Permissions

Now we've got managed code that needs to run on the machine with FullTrust, otherwise it won't have permission to modify the permission policy (it needs to be running as a Win32 Administrator as well). Placing an EXE on a Web server and asking users to click on a link won't do because then the code will be in a partially trusted environment and we're back where we started. The easiest way to package up managed code for execution on the client machine with FullTrust is using an MSI. MSI files are executed by a runtime engine that downloads the code to the machine before running it, thereby giving us the permissions we need to award other permissions. Also, MSI files have built in support in the intranet world for deployment tools like SMS and Active Directory. In a less sophisticated deployment environment, like smaller businesses or the Internet, you can provide a link that executes the MSI file on the user's machine after a prompt. Even if each of your users has to run the MSI themselves, that's still only one install to enable the permissions needed for every smart client deployed from the IT group's internal Web site, for example.

There are many tools for building MSI files, but the one most readily available comes with advanced versions of Visual Studio® .NET. The trick is to convince a setup project to execute your code during installation. Assuming you've got a Visual Studio .NET solution with a setup project and a class library project, you only have two major tasks left to do this convincing.

The first task is to add a class to your class library project that derives from System.Configuration.Install.Installer and is tagged with the RunInstaller(true) attribute. An instance of any such class is created by the MSI engine during setup, so that's where you put your custom code. The easiest way to get such a class is to right-click on your class library project in the Solution Explorer and choosing Add New Item | Code | Installer Class. It creates a place for your permission award code in the constructor, like so:

[RunInstaller(true)]
public class Installer1 : System.Configuration.Install.Installer {
  public Installer1() {
    ...

    // TODO: Add your permission award code here
  }
}

Your second task is to add this assembly to the list of custom actions that you setup during installation. To do that, right-click on your setup project in the Solution Explorer and select View, then Custom Actions. This shows you a list of the custom actions at each phase of the setup, as shown in Figure 3.

Figure 3. Setup Project Custom Actions

To add a custom action to the install phase, right-click on the Install custom action list and choose Add Custom Action, which shows you the list of folders to place your custom action code into, as shown in Figure 4.

Figure 4. Selection a folder for a custom action

Double-click the Application Folder and select Add Output to choose the output from one of the other projects in the solution. Make sure the class library project with your installer class is selected at the top and choose Primary Output, as shown in Figure 5.

Figure 5. Choosing the Primary Output of your class library project to act as a custom action

These settings cause the installer classes in your class library assembly to be created during the Install phase of your MSI setup. Now, when you build and execute the MSI file produced by the setup project, your code will execute at FullTrust and can award permissions for your assemblies.

Conclusion

The limited permissions provided by default permission sets give us additional confidence in the security of code executed from places other than our machine. This keeps smart clients from being the world's best virus construction kit. However, for certain scenarios, we'd like well-known assemblies or sites to have additional permissions to provide users with additional services. This can be accomplished with new code groups that match membership conditions of assemblies with default or custom permission sets that award more permissions. .NET provides an object model for creating code groups and permission sets on demand that can be packaged into MSI files for deployment to your users. This technique gives you the benefits of both the smart client HTTP-style deployment model and the permissions your applications need to satisfy the requirements of your users.

Acknowledgements

As ever, Keith Brown is the security wind beneath my wings. He wrote the most excellent FindAPTC utility that reports which .NET assemblies are marked APTC and which are not. He figured out the problem in the smart client hosting code that requires the minimal Execution permission set for an entire site. He clued me into the difference between creating a NamedPermissionSet with and without PermissionState.None. Keith, did you ever know that you're my hero? You're everything that I'd like to be.

References

Chris Sells is an independent consultant, specializing in distributed applications in .NET and COM, as well as an instructor for DevelopMentor. He's written several books, including ATL Internals, which is in the process of being updated for ATL7. He's also working on Essential Windows Forms for Addison-Wesley and Mastering Visual Studio .NET for O'Reilly. In his free time, Chris hosts the Web Services DevCon and directs the Genghis source-available project. More information about Chris, and his various projects, is available at http://www.sellsbrothers.com.