Share via


Do You Trust It?

Discover Techniques for Safely Hosting Untrusted Add-Ins with the .NET Framework 2.0

Shawn Farkas

This article is based on a prerelease version of the .NET Framework 2.0. All information herein is subject to change.

This article discusses:
  • Designing support for add-ins
  • Application domain sandboxing
  • New APIs for add-in support
  • Advanced add-in hosting
This article uses the following technologies:
.NET Framework 2.0

Contents

A Basic Add-In Model
Using Full Assembly Names
AppDomain Sandboxing
Changing the ApplicationBase
A Robust API for Add-Ins
Advanced Hosting Techniques
Conclusion

Through the use of dynamic assembly loading and reflection, managed code provides a way for you to easily allow your application to be extended by add-ins. However, when you allow your application to run arbitrary code through an add-in model, you expose the user's computer to potentially unknown code, running the risk that malicious code will use your application as an entry point to the user's data. There are several techniques you can use to reduce the attack surface of your application and protect against malicious or otherwise unstable code corrupting your user's computer—or possibly stealing sensitive data.

In this article, I'll explore a hypothetical application that supports add-ins, and examine how the application might be augmented to provide additional features and functionality to the user while also helping to protect the user's computer from malicious or unstable code.

A Basic Add-In Model

In its simplest form, an application's add-in model is little more than an interface or set of interfaces defined in a shared library. Add-ins implement the interface, and notify the application of their existence, possibly by registering themselves in a configuration file or by placing themselves in a separate directory created specifically for add-ins.

In order to create one of these add-ins, the application would simply call Activator.CreateInstance, specifying the assembly containing the add-in and the type that implements the common interface. By casting the resulting instantiated object to the well-known interface, you can then invoke its functionality, as demonstrated in Figure 1.

Figure 1 Flawed Add-In

In AddInManager.dll

public interface IAddIn
{
    void Start();
    ...
}

In Application.exe

IAddIn addIn = Activator.CreateInstance(
    "AddInAssembly",
    "AddInNamespace.AddInType") as IAddIn;
if(addIn != null)
{
    addIn.Start();
}

While this method will work, it has many potential pitfalls. For instance, by providing only a simple assembly name to Activator.CreateInstance, you know that you're loading an assembly named AddInAssembly, but you don't necessarily know whether you're loading the add-in that your user wants.

Using Full Assembly Names

Instead of using simple assembly names to load add-ins, you should always refer to them by their strong name. This prevents a scenario where a malicious assembly with the same name as a useful add-in is accidentally loaded by your application. Since the strong name of an assembly contains the public-key token that the assembly was signed with, you know that the assembly you're loading was created by someone with access to the private key of the add-in you intended to load.

You should also make sure to give strong names to all of the assemblies in your hosting application. By strongly naming your assemblies, you can ensure that add-ins bind to the version of your application that they were written against. If you need to release side-by-side versions of your app, then add-ins for version 1 of your host will bind against the version 1 add-in architecture, while add-ins for version 2 will bind against the version 2 architecture.

The simple add-in mechanism shown in Figure 1 has an even more obvious flaw than using simple names, however. In the current model, you're simply passing control of your application over to untrusted code that could do anything.

Since the add-in probably resides in a DLL on the local computer, the default security policy is going to grant that code full trust. This means that even though the add-in implements the IAddIn interface and claims to want to work within your add-in architecture, nothing is preventing it from accessing the user's Microsoft® Money files, for example, and copying them up to a Web site inside of its Start method.

Even if the add-in isn't explicitly malicious, it could be poorly coded. If something inside the Start method causes the add-in to crash (say, by throwing an unhandled exception), it has the potential to bring down your entire application with it—possibly causing any changes the user made to her document to be lost.

AppDomain Sandboxing

The basic unit of isolation in the common language runtime (CLR) is the application domain, represented by the System.AppDomain class. It's common to describe an application domain, or app domain, as a sort of lightweight process because code executing inside one app domain does not affect code executing inside another. In fact, unless they go through remoting or unmanaged code, they can't even access each other.

Another interesting aspect of an app domain is that it can be used to control the permission sets granted to different assemblies. When an app domain is used to run an assembly with a well-known, safe set of permissions, that app domain is generally referred to as a sandbox.

In version 1.x of the CLR, setting up a sandboxed app domain took significant effort. You had to create an AppDomain policy level, create a series of code groups, and define the permission sets that would be granted to each one. Thankfully, the .NET Framework 2.0 introduces a simple sandboxing API in the form of a new overload of the static AppDomain.CreateDomain method.

The simple sandboxing API creates an AppDomain that has two distinct permission sets. Assemblies from the Global Assembly Cache (GAC) receive full trust, while every other assembly receives a set of permissions that you specify at AppDomain creation time. The AppDomain itself is also configured with the specified permission set. Finally, you can specify a set of assemblies that do not reside in the GAC, but that should be granted full trust anyway. The API takes the five parameters described in Figure 2.

Figure 2 Sandboxing Parameters

Parameter Type Use
friendlyName string Name of the AppDomain
securityInfo Evidence Evidence for the AppDomain
info AppDomainSetup Extra configuration settings for the AppDomain
grantSet PermissionSet Default permission set to grant assemblies and the domain itself
fullTrustAssemblies StrongName[] Assemblies to receive FullTrust in addition to all assemblies from the GAC

Note that the evidence used for the domain will never be used for code access security (CAS) policy evaluation. In fact, assemblies loaded into this type of AppDomain are never resolved against the security policy, since you've already specified the permission set that they should be granted. This means that even if you modify your machine policy such that it would grant extra permissions to an add-in you trust, when the add-in is loaded into an AppDomain created with the sandboxing API, the add-in won't receive those permissions.

A basic sandboxed application domain that grants the Internet permission set to code loaded into it might look something like the code in Figure 3. If you attempt to use that code, however, you'll notice a quirk in the parameters for CreateDomain. As is, this code will throw an InvalidOperationException, saying "This API requires the ApplicationBase to be specified explicitly in the AppDomainSetup parameter." The ApplicationBase property affects where the CLR looks for assemblies when attempting to load them. A quick fix for the exception would be to set the ApplicationBase to be the same as that of the default domain:

setup.ApplicationBase = 
    AppDomain.CurrentDomain.SetupInformation.ApplicationBase;

By adding that bit of code, you ensure the sandboxed application domain will be created successfully.

Figure 3 AppDomain with Internet Permissions

// get the set of permissions granted to code from the Internet.
Evidence internetEvidence = new Evidence(
    new object[] {  new Zone(SecurityZone.Internet) }, 
    new object[] {  });

PermissionSet internetPermissions = 
    SecurityManager.ResolvePolicy(internetEvidence);

// create a sandboxed domain
AppDomainSetup setup = new AppDomainSetup();
AppDomain sandboxedDomain = AppDomain.CreateDomain(
    "Add-In Domain", internetEvidence, setup, 
    internetPermissions, new StrongName[] { });

Now you'll need to load the add-in into that domain, and let it do its work. One way to do this is to define an add-in manager class that gets loaded into the sandbox and invokes the add-in for you (see Figure 4). This class should derive from MarshalByRefObject so that it can take advantage of remoting to communicate between the host AppDomain and the sandboxed domain (see Figure 5). This is a big improvement over the previous model, since now all add-ins are limited to the Internet permission set, and a crash in one add-in will not crash the entire application.

Figure 4 Creating an Add-In Manager

Invoking the Add-In in the Sandbox

Type addInManagerType = typeof(AddInManager);
AddInManager manager = (AddInManager)
    sandboxedDomain.CreateInstanceAndUnwrap(
        addInManagerType.Assembly.FullName,
        addInManagerType.FullName);
manager.RunAddIn(addInAsembly, addInType, currentDocument);

In AddInManager.dll

public class AddInManager : MarshalByRefObject
{
    public void RunAddIn(string addInAssembly, string addInType)
    {
        IAddIn addIn = Activator.CreateInstance(addInAssembly, 
            addInType) as IAddIn;
        if(addIn != null) addIn.Start();
    }
}

While the basic AddInManager might work, doing anything but the most basic tasks will probably cause a SecurityException since every assembly in the sandboxed app domain will be granted the Internet permission set unless it's located in the GAC. To grant FullTrust permissions to AddInManager, you'll need to add its strong name to the FullTrust list.

Figure 5 Communicating Across Domains

Figure 5** Communicating Across Domains **

The CLR does not provide an easy method for getting a StrongName object for an assembly, although the code to do so is relatively straightforward, as shown in Figure 6. By adding the strong name of AddInManager.dll to the FullTrust list when creating the sandboxed domain, you ensure that the code will be free to do what it needs to while still limiting the add-in's permission set.

Figure 6 Finding the Strong Name for an Assembly

public static StrongName GetStrongName(Assembly assembly)
{
    if(assembly == null)
        throw new ArgumentNullException("assembly");

    AssemblyName assemblyName = assembly.GetName();
        
    // get the public key blob
    byte[] publicKey = assemblyName.GetPublicKey();
    if(publicKey == null || publicKey.Length == 0)
        throw new InvalidOperationException(
            String.Format("{0} is not strongly named", 
            assembly));
    
    StrongNamePublicKeyBlob keyBlob = 
        new StrongNamePublicKeyBlob(publicKey);

    // create the StrongName
    return new StrongName(
        keyBlob, assemblyName.Name, assemblyName.Version);
}

Keep in mind that the sandboxed application domain itself has the restricted permission set. This means any security demand that causes a stack walk to attempt to leave the sandboxed domain will have to be a subset of the sandboxed permission set. In order to prevent this from hampering your add-in code, you'll need to assert FullTrust before doing any privileged operations.

When asserting FullTrust, you should always remember to revert the assert as soon as the privileged operation is complete. For instance, if you needed to read some configuration information out of a database in your add-in manager, the code might look like Figure 7. Remember that any time you assert permissions, make sure that you're not vulnerable to a luring attack where untrusted code can affect what you do while operating with full trust.

Figure 7 Asserting Permissions

public class AddInManager : MarshalByRefObject
{
    private static void ReadConfiguration()
    {
        ... // prepare to connect to the database here

        new PermissionSet(PermissionState.Unrestricted).Assert();
        ... // read configuration information from the database 
        CodeAccessPermission.RevertAssert();

        ... // process configuration information here
    }
    ...
}

Changing the ApplicationBase

Recall that my first attempt to create a sandboxed domain (see Figure 3) caused an InvalidOperationException because I had not set the ApplicationBase for the application domain. Since setting the ApplicationBase to be the same as the hosting application's ApplicationBase can have unintended consequences, the CLR needs you to explicitly set this property rather than using the ApplicationBase of the host AppDomain as the default.

The problem with using the same ApplicationBase as the host is that a partial trust assembly can use the Assembly.Load method to load any assembly that resides in the ApplicationBase. Once the assembly is loaded, the partial-trust code can then access any public types or methods defined in it. Generally, the hosting application's classes will not be intended to be used in this fashion. And it almost certainly won't have been tested in these conditions.

In order to prevent this repurposing attack, you need to prevent the partial-trust code from loading the host assemblies. The best way to accomplish this goal is to ensure that the add-ins are physically located in a different directory than the hosting application. Then, when creating the sandboxed domain, set the ApplicationBase to point to the add-in directory (see Figure 8).

Figure 8 Preventing a Repurposing Attack

Figure 8** Preventing a Repurposing Attack **

There are two common schemes for organizing the layout of an add-in. The first is to place all add-in assemblies in a subdirectory of the hosting application (for example, \AppDir\AddIns). Another scheme is to have a separate subdirectory for each add-in. By separating the add-ins from each other, you prevent them from being able to load each other at run time.

Deciding between these two schemes is largely dependent upon the goals of your application. Separate directories for add-ins allow you to isolate each add-in from the others, but comes at the cost of requiring a new AppDomain for each add-in. On the other hand, placing every add-in in the same directory allows you to use only a single sandboxed AppDomain, but leaves the add-ins free to interfere with each other.

The first problem that you'll run into when moving add-ins to their own ApplicationBase is that the very issue you were trying to solve now comes back to bite you. Since the sandboxed AppDomain can no longer access host assemblies, there is no way for your AddInManager.dll to be loaded. There are three obvious solutions to this problem:

  • Have several copies of AddInManager.dll, one in your application's directory and one in each add-in directory.
  • Place AddInManager.dll in the GAC.
  • Use LoadFrom to direct the loader to the exact path of your AddInManager.dll.

Clearly the first method doesn't scale very well, especially if you choose to have a separate subdirectory for each add-in. In addition to wasting disk space, you now have a servicing nightmare if you need to deploy a security update to your AddInManager library.

By placing AddInManager in the GAC, you can avoid using LoadFrom. This can be desirable since in some situations LoadFrom can cause problems that are difficult to debug because it uses a different loading context than Load. However, since the assembly may need to be used by partially trusted code, you'll probably have to apply the AllowPartiallyTrustedCallers attribute. Now that it's in the GAC, any managed code that runs on the machine can access the assembly, so you'll need to do a security review and test your code for the cases in which it's not running as part of your app.

Using LoadFrom presents quite a different problem. LoadFrom requires FileIOPermission for the assembly being loaded. The sandboxed AppDomain won't have this permission—otherwise someone could just LoadFrom your host assemblies, and you would not have solved anything by changing the ApplicationBase. Calling the standard AppDomain.CreateInstanceFrom method will cause a SecurityException when the demand hits the AppDomain boundary.

In order to solve this problem, the .NET Framework 2.0 introduces a new overload of Activator.CreateInstanceFrom. This overload takes an AppDomain to create the instance in, the path to the assembly, and the type to create. This overload is special because, before loading the assembly, it does an assert for full trust in order to prevent the demand for FileIOPermission from failing. This method would be used like this:

// create an add-in manager to invoke the add-in in the sandbox
Type addInManagerType = typeof(AddInManager);
AddInManager manager = (AddInManager)Activator.CreateInstanceFrom(
    sandboxedDomain,
    addInManagerType.Assembly.Location,
    addInManagerType.FullName).Unwrap();
manager.RunAddIn(addInAsembly, addInType);

There's also a corresponding version of CreateInstance that behaves in a similar way. If you need to pass parameters to the constructor of your object, or want to affect the way the runtime binds to your type, there are other overloads that provide such functionality. The rule of thumb is that any method on Activator that takes an AppDomain as a parameter is going to operate under an assert for full trust.

While these methods can be convenient, they can also be dangerous if not used carefully. When these methods are used, the constructor of the type being instantiated will run with no partial- trust boundary on the call stack. This has several implications that your application needs to be hardened against.

First, you should not pass any untrusted objects as parameters to your constructor. These objects could implement custom serialization, which would be invoked when they were transferred across the AppDomain boundary. Since there's no way for the CLR to ensure that the object's serialization code is actually doing the work of serialization, passing untrusted objects as parameters gives these objects a window to do just about anything they want. Since there's nothing but FullTrust frames on the call stack, the serialization code is free to upload your user's Microsoft Money files (or any other files) to the Internet again.

Another related issue has to do with exceptions. The constructor for your AddInManager object needs to make sure no untrusted exceptions leave its stack frame. Since the CLR will attempt to move the exception from the sandboxed application domain back into your host domain, the exception could use its serialization code to run with full trust.

Not allowing any untrusted exceptions out of your constructor means that you need to have a blanket try/catch around any calls to partial-trust code. This includes not only direct calls to methods in untrusted assemblies, but also calls to virtual methods that could have been overridden by untrusted code and calls that will end up invoking delegates that could come from the untrusted code. In general, it's safest to do as little work as possible in your constructor and to avoid calling any untrusted code.

A Robust API for Add-Ins

In all but the simplest applications, you'll want to provide an object model with which your add-ins can interact. This object model will probably consist of at least one System.Security.AllowPartiallyTrustedCallersAttribute (APTCA) assembly, which exposes several classes the add-ins can use to interact with your application and its open documents. Since this assembly will be marked with the APTCA attribute, security reviews are vital. If your object model grows to any significant size, this can be time-consuming, but absolutely necessary to make sure your app is not exposing a path for untrusted code to elevate its permissions.

Thankfully, the .NET Framework 2.0 provides transparent assemblies that help to reduce the attack surface and the amount of code that must be thoroughly audited. A transparent assembly voluntarily gives up its ability to use constructs such as unverifiable code and asserts. Transparent code also cannot satisfy a LinkDemand. Due to these restrictions, CAS demands just propagate through the transparent code, even if it is a FullTrust assembly. However, a partial-trust transparent assembly may still cause a demand to fail. Since a transparent assembly cannot be used by partially trusted code to increase its effective permission set, any assemblies that are marked transparent do not require as thorough of a security audit as standard APTCA assemblies.

In order to reduce the attack surface of your application, one good way to structure your object model would be to create most of it in a transparent assembly. Then some security-critical code can be used to broker access from the transparent assembly to the rest of the application. This minimizes the amount of security-critical code that needs review.

Assemblies are marked transparent with the SecurityTransparentAttribute. Once the assembly has been marked transparent, no security-critical code can reside within it. If you need to mix security-critical and transparent code in the same assembly, you should instead apply the SecurityCriticalAttribute to the assembly, which makes the assembly critical, but does not automatically apply to all code within the assembly. Instead, you must mark all critical code with its own SecurityCriticalAttribute. This makes everything in the assembly not specifically marked with a SecurityCriticalAttribute transparent, but gives you the flexibility to mark specific types and methods as security-critical.

If you do want to have the SecurityCriticalAttribute propagate to everything scoped beneath its target, you should pass it a parameter of SecurityCriticalScope.Everything. For instance, if you marked a class with SecurityCriticalScope.Everything, all nested types, methods, and properties within the class would also be marked as security-critical.

As an example of designing with a split between transparent and critical code, let's consider the object model that a text editor might expose. The editor could have an Application class with a CurrentDocument property exposing the document the user is currently working on. Once obtained, the Document class lets the add-in obtain its size, which doesn't require any special permissions. However, it also allows the add-in to attempt saving the document. Since saving will require access to the file system, it will require elevated permissions. The resulting object model might look, in part, like Figure 9.

Figure 9 Application Class

[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityCritical]

public sealed class Application {
    public static Application Instance { get { return currentInstance; } }
    public Document CurrentDocument { get { return currentDocument; } }
    ...
}

public sealed class Document {
    public int Size { get { return contents.Length; } }
    
    [SecurityCritical]
    public void Save()
    {
        if(EnsureUserWantsToSave()) {
            new FileIOPermission(PermissionState.Unrestricted).Assert();
            WriteDocumentToDisk();
            CodeAccessPermission.RevertAssert();
        }
    }
    ...
}

In this case, since the assembly contains some critical code, the entire assembly requires a SecurityCriticalAttribute instead of a SecurityTransparentAttribute. However, since Application doesn't do any security actions, it doesn't get a SecurityCritcalAttribute and is transparent. If you attempted to do an Assert in one of Application's methods, you would get an InvalidOperationException with a message of "Cannot perform CAS Asserts in Security Transparent assemblies." The message is slightly misleading, since the assembly itself is not transparent, but the issue is that all code within the Application class is transparent.

Since the Save method of the Document class requires an Assert for FileIOPermission, that method must be marked critical. Within the method, the window of time that the Assert is in effect is kept as short as possible.

Advanced Hosting Techniques

The .NET Framework 2.0 provides the ability for applications to hook and customize many important CLR events, such as the creation of AppDomains and the resolution of security policy. The entry point to these features is the AppDomainManager class, which can be subclassed by applications wishing to customize the way the CLR handles these events. The AppDomainManager provides many methods and properties that can be overridden depending on the behavior you want to customize. A good overview of the options available can be found in Steven Pratschner's book, Customizing the Microsoft .NET Framework Common Language Runtime (Microsoft Press®, 2005)

For the purposes of add-in management, one of the more interesting events that you can hook is CreateDomain. By intercepting every call to CreateDomain, you can create a custom domain allocation policy. For instance, instead of having only one AppDomain for all add-ins to run in, you could put every add-in signed with the same key into the same AppDomain.

In order to implement this, you could attach the strong name of the add-in assembly that the AppDomain is for to the AppDomain's evidence and then call CreateDomain. Your AppDomainManager would then override the CreateDomain method and determine whether this public key already has an AppDomain or a new one should be created. Figure 10 shows how this could be implemented.

Figure 10 Custom Domain Allocation

// create Evidence for the add-in being loaded
Assembly addInAssembly = Assembly.ReflectionOnlyLoadFrom(addInPath);
Evidence addinEvidence = new Evidence(
    new object[] { GetStrongName(addInAssembly) }, 
    new object[] {  });

// create a sandboxed domain
AppDomain sandboxedDomain = AppDomain.CreateDomain("Add-In Domain", 
    addinEvidence, setup, internetPermissions, hostAssemblies);

public sealed class AddInAppDomainManager : AppDomainManager
{
    private Dictionary<StrongNamePublicKeyBlob, AppDomain> appDomains = 
        new Dictionary<StrongNamePublicKeyBlob, AppDomain>();

    public override AppDomain CreateDomain(string friendlyName, 
        Evidence securityInfo, AppDomainSetup appDomainInfo)
    {
        // find the strong name evidence
        StrongName strongName = null;
        foreach(object evidence in securityInfo)
        {
            strongName = evidence as StrongName;
            if(strongName != null) break;
        }

        if(strongName == null)
            throw new InvalidOperationException("Attempt to create " +
                "an AppDomain without add-in StrongName evidence");

        // see if we already have an AppDomain for this key
        if(appDomains.ContainsKey(strongName.PublicKey))
            return appDomains[strongName.PublicKey];
            
        // if not, create a new one
        AppDomain newDomain = CreateDomainHelper(
            friendlyName, securityInfo, appDomainInfo);
        appDomains[strongName.PublicKey] = newDomain;
        return newDomain;
    }
}

There are a couple of things to notice about this code. First, when loading the add-in assembly to get its strong name, the code only loads it for Reflection. The ReflectionOnlyLoadFrom method was added to the .NET Framework 2.0 to provide a way to inspect an assembly while being assured that no code from the assembly will be able to run.

Second, since AppDomainManager itself is managed code, it requires an AppDomain in which to execute. This means CreateDomain won't be called for the default domain, which is a good thing because you don't want to use your customization there.

Now that you've got an AppDomainManager, you need tell the CLR to load it. The easiest way to do this is to set the APPDOMAIN_MANAGER_ASM environment variable to be the full strong name of the assembly containing the AppDomainManager, and to set the APPDOMAIN_MANAGER_TYPE environment variable to be the namespace and type that derives from AppDomainManager. Note that the CLR requires AppDomainManger to be fully trusted and strongly named.

Conclusion

The ability to easily run add-in code within your app is one of the more powerful features of the .NET Framework. It can also be one of the more dangerous features if not used properly.

Always run untrusted code in a sandboxed AppDomain, with an ApplicationBase that is not the same as the ApplicationBase of your main application. If there is no partial-trust stack frame currently on the call stack, make sure that you do not allow custom serializers to run by passing untrusted objects across AppDomain boundaries or allowing untrusted exceptions to be marshaled across AppDomains. When providing an object model for the add-in code to program against, try to keep as much of it as possible transparent, only using security-critical code where absolutely necessary. When you follow these guidelines, you can provide a rich extensibility story for your applications, while keeping your user's data safe from being damaged by malicious code.

Shawn Farkas is a software design engineer in test on the CLR security team at Microsoft. He works on ClickOnce, cryptography, and IL verification. See his blog at blogs.msdn.com/shawnfa.