60

I am looking for a simple and secure way to access plugins from a .NET application. Although I imagine that this is a very common requirement, I am struggling to find anything that meets all my needs:

  • The host application will discover and load its plugin assemblies at runtime
  • Plugins will be created by unknown 3rd parties, so they must be sandboxed to prevent them from executing malicious code
  • A common interop assembly will contain types that are referenced by both the host and its plugins
  • Each plugin assembly will contain one or more classes that implement a common plugin interface
  • When initializing a plugin instance, the host will pass it a reference to itself in the form of a host interface
  • The host will call into the plugin via its common interface and the plugins may call into the host likewise
  • The host and the plugins will exchange data in the form of the types defined in the interop assembly (including generic types)

I have investigated both MEF and MAF, but I am struggling to see how either of them can be made to fit the bill.

Assuming my understanding is correct, MAF is unable to support the passing of generic types across its isolation boundary, which is essential to my application. (MAF is also very complex to implement, but I would be prepared to work with this if I could solve the generic type problem).

MEF is almost a perfect solution, but appears to fall short on the security requirement, as it loads its extension assemblies in the same AppDomain as the host, and thus apparently prevents sandboxing.

I have seen this question, which talks of running MEF in a sandboxed mode, but doesn't describe how. This post states that "when using MEF you must trust extensions not to run malicious code, or offer protection via Code Access Security" but, again, it doesn't describe how. Finally, there is this post, which describes how to prevent unknown plugins from being loaded, but this is not appropriate to my situation, as even legitimate plugins will be unknown.

I have succeeded in applying .NET 4.0 security attributes to my assemblies and they are correctly respected by MEF, but I don't see how this helps me to lock out malicous code, as many of the framework methods that might be a security threat (such as methods of System.IO.File) are marked as SecuritySafeCritical, which means that they are accessible from SecurityTransparent assemblies. Am I missing something here? Is there some additonal step I can take to to tell MEF that it should provide internet privileges to plugin assemblies?

Finally, I have also looked at creating my own simple sandboxed plugin architecture, using a separate AppDomain, as described here. However, as far as I can see, this technique only allows me to use late binding to invoke static methods on classes in an untrusted assembly. When I try to extend this approach to create an instance of one of my plugin classes, the returned instance cannot be cast to the common plugin interface, which means that it is impossible for the host application to call into it. Is there some technique I can use to get strongly-typed proxy access across the AppDomain boundary?

I apologize for the length of this question; the reason was to show all the avenues that I have already investigated, in the hope that somebody can suggest something new to try.

Many thanks for your ideas, Tim

Community
  • 1
  • 1
Tim Coulter
  • 8,705
  • 11
  • 64
  • 95

5 Answers5

55

I have accepted Alastair Maw's answer, as it was his suggestion and links that led me to a workable solution, but I am posting here some details of exactly what I did, for anyone else who may be trying to achieve something similar.

As a reminder, in its simplest form my application comprises three assemblies:

  • The main application assembly that will consume plugins
  • An interop assembly that defines common types shared by the application and its plugins
  • A sample plugin assembly

The code below is a simplified version of my real code, showing only what is required to discover and load plugins, each in its own AppDomain:

Starting with the main application assembly, the main program class uses a utility class named PluginFinder to discover qualifiying plugin types within any assemblies in a designated plugin folder. For each of these types, it then creates an instance of a sandox AppDomain (with internet zone permissions) and uses it to create an instance of the discovered plugin type.

When creating an AppDomain with limited permissions, it is possible to specify one or more trusted assemblies that are not subject to those permissions. To accomplish this in the scenario presented here, the main application assembly and its dependencies (the interop assembly) must be signed.

For each loaded plugin instance, the custom methods within the plugin can be called via its known interface and the plugin can also call back to the host application via its known interface. Finally, the host application unloads each of the sandbox domains.

class Program
{
    static void Main()
    {
        var domains = new List<AppDomain>();
        var plugins = new List<PluginBase>();
        var types = PluginFinder.FindPlugins();
        var host = new Host();

        foreach (var type in types)
        {
            var domain = CreateSandboxDomain("Sandbox Domain", PluginFinder.PluginPath, SecurityZone.Internet);
            plugins.Add((PluginBase)domain.CreateInstanceAndUnwrap(type.AssemblyName, type.TypeName));
            domains.Add(domain);
        }

        foreach (var plugin in plugins)
        {
            plugin.Initialize(host);
            plugin.SaySomething();
            plugin.CallBackToHost();

            // To prove that the sandbox security is working we can call a plugin method that does something
            // dangerous, which throws an exception because the plugin assembly has insufficient permissions.
            //plugin.DoSomethingDangerous();
        }

        foreach (var domain in domains)
        {
            AppDomain.Unload(domain);
        }

        Console.ReadLine();
    }

    /// <summary>
    /// Returns a new <see cref="AppDomain"/> according to the specified criteria.
    /// </summary>
    /// <param name="name">The name to be assigned to the new instance.</param>
    /// <param name="path">The root folder path in which assemblies will be resolved.</param>
    /// <param name="zone">A <see cref="SecurityZone"/> that determines the permission set to be assigned to this instance.</param>
    /// <returns></returns>
    public static AppDomain CreateSandboxDomain(
        string name,
        string path,
        SecurityZone zone)
    {
        var setup = new AppDomainSetup { ApplicationBase = Path.GetFullPath(path) };

        var evidence = new Evidence();
        evidence.AddHostEvidence(new Zone(zone));
        var permissions = SecurityManager.GetStandardSandbox(evidence);

        var strongName = typeof(Program).Assembly.Evidence.GetHostEvidence<StrongName>();

        return AppDomain.CreateDomain(name, null, setup, permissions, strongName);
    }
}

In this sample code, the host application class is very simple, exposing just one method that may be called by plugins. However, this class must derive from MarshalByRefObject so that it can be referenced between application domains.

/// <summary>
/// The host class that exposes functionality that plugins may call.
/// </summary>
public class Host : MarshalByRefObject, IHost
{
    public void SaySomething()
    {
        Console.WriteLine("This is the host executing a method invoked by a plugin");
    }
}

The PluginFinder class has only one public method that returns a list of discovered plugin types. This discovery process loads each assembly that it finds and uses reflection to identify its qualifying types. Since this process may potentially load many assemblies (some of which are do not even contain plugin types) it is also executed in a separate application domain, which may be subsequntly unloaded. Note that this class also inherits MarshalByRefObject for the reasons described above. Since instances of Type may not be passed between application domains, this discovery process uses a custom type called TypeLocator to store the string name and assembly name of each discovered type, which may then be safely passed back to the main applicatin domain.

/// <summary>
/// Safely identifies assemblies within a designated plugin directory that contain qualifying plugin types.
/// </summary>
internal class PluginFinder : MarshalByRefObject
{
    internal const string PluginPath = @"..\..\..\Plugins\Output";

    private readonly Type _pluginBaseType;

    /// <summary>
    /// Initializes a new instance of the <see cref="PluginFinder"/> class.
    /// </summary>
    public PluginFinder()
    {
        // For some reason, compile-time types are not reference equal to the corresponding types referenced
        // in each plugin assembly, so equality must be tested by loading types by name from the Interop assembly.
        var interopAssemblyFile = Path.GetFullPath(Path.Combine(PluginPath, typeof(PluginBase).Assembly.GetName().Name) + ".dll");
        var interopAssembly = Assembly.LoadFrom(interopAssemblyFile);
        _pluginBaseType = interopAssembly.GetType(typeof(PluginBase).FullName);
    }

    /// <summary>
    /// Returns the name and assembly name of qualifying plugin classes found in assemblies within the designated plugin directory.
    /// </summary>
    /// <returns>An <see cref="IEnumerable{TypeLocator}"/> that represents the qualifying plugin types.</returns>
    public static IEnumerable<TypeLocator> FindPlugins()
    {
        AppDomain domain = null;

        try
        {
            domain = AppDomain.CreateDomain("Discovery Domain");

            var finder = (PluginFinder)domain.CreateInstanceAndUnwrap(typeof(PluginFinder).Assembly.FullName, typeof(PluginFinder).FullName);
            return finder.Find();
        }
        finally
        {
            if (domain != null)
            {
                AppDomain.Unload(domain);
            }
        }
    }

    /// <summary>
    /// Surveys the configured plugin path and returns the the set of types that qualify as plugin classes.
    /// </summary>
    /// <remarks>
    /// Since this method loads assemblies, it must be called from within a dedicated application domain that is subsequently unloaded.
    /// </remarks>
    private IEnumerable<TypeLocator> Find()
    {
        var result = new List<TypeLocator>();

        foreach (var file in Directory.GetFiles(Path.GetFullPath(PluginPath), "*.dll"))
        {
            try
            {
                var assembly = Assembly.LoadFrom(file);

                foreach (var type in assembly.GetExportedTypes())
                {
                    if (!type.Equals(_pluginBaseType) &&
                        _pluginBaseType.IsAssignableFrom(type))
                    {
                        result.Add(new TypeLocator(assembly.FullName, type.FullName));
                    }
                }
            }
            catch (Exception e)
            {
                // Ignore DLLs that are not .NET assemblies.
            }
        }

        return result;
    }
}

/// <summary>
/// Encapsulates the assembly name and type name for a <see cref="Type"/> in a serializable format.
/// </summary>
[Serializable]
internal class TypeLocator
{
    /// <summary>
    /// Initializes a new instance of the <see cref="TypeLocator"/> class.
    /// </summary>
    /// <param name="assemblyName">The name of the assembly containing the target type.</param>
    /// <param name="typeName">The name of the target type.</param>
    public TypeLocator(
        string assemblyName,
        string typeName)
    {
        if (string.IsNullOrEmpty(assemblyName)) throw new ArgumentNullException("assemblyName");
        if (string.IsNullOrEmpty(typeName)) throw new ArgumentNullException("typeName");

        AssemblyName = assemblyName;
        TypeName = typeName;
    }

    /// <summary>
    /// Gets the name of the assembly containing the target type.
    /// </summary>
    public string AssemblyName { get; private set; }

    /// <summary>
    /// Gets the name of the target type.
    /// </summary>
    public string TypeName { get; private set; }
}

The interop assembly contains the base class for classes that will implement plugin functionality (note that it also derives from MarshalByRefObject.

This assembly also defines the IHost interface that enables plugins to call back into the host application.

/// <summary>
/// Defines the interface common to all untrusted plugins.
/// </summary>
public abstract class PluginBase : MarshalByRefObject
{
    public abstract void Initialize(IHost host);

    public abstract void SaySomething();

    public abstract void DoSomethingDangerous();

    public abstract void CallBackToHost();
}

/// <summary>
/// Defines the interface through which untrusted plugins automate the host.
/// </summary>
public interface IHost
{
    void SaySomething();
}

Finally, each plugin derives from the base class defined in the interop assembly and implements its abstract methods. There may be multiple inheriting classes in any plugin assembly and there may be multiple plugin assemblies.

public class Plugin : PluginBase
{
    private IHost _host;

    public override void Initialize(
        IHost host)
    {
        _host = host;
    }

    public override void SaySomething()
    {
        Console.WriteLine("This is a message issued by type: {0}", GetType().FullName);
    }

    public override void DoSomethingDangerous()
    {
        var x = File.ReadAllText(@"C:\Test.txt");
    }

    public override void CallBackToHost()
    {
        _host.SaySomething();           
    }
}
Caleb
  • 289
  • 3
  • 17
Tim Coulter
  • 8,705
  • 11
  • 64
  • 95
  • 6
    This is a great write-up. Good on you for finding the time to share all that. :) – Alastair Maw Nov 13 '10 at 13:10
  • This is really cool! There is just one little suggestion. If you want to overcome the workaround with preloading the sandboxing assembly in the PluginFinder ctor, here is what to do: Introduce an interface IPlugin that has all methods from PluginBase. Make PluginBase implement it (it already fulfills it). Then change your type matching logic from: if (!type.Equals(_pluginBaseType) && _pluginBaseType.IsAssignableFrom(type)) to: //assuming "Sandboxing" is the namespace if (type.GetInterface("Sandboxing.IPlugin") != null && !type.IsAbstract) – Christoph Jun 22 '12 at 12:56
  • Downside of the approach I described in my previous comment is that plugin authos might think it would be enough to implement IPlugin where as the truth is that they still need to derive from PluginBase. – Christoph Jun 22 '12 at 13:30
  • I have been trying to implement an architecture to implement the same need you list in the original post. I tried to use exactly what you have written up here, but I still can't isolate the plugin enough to when a plugin crashes it doesn't crash the host. When I call the DoSomethingDangerous method I have to catch the exception on the host or it will crash. Worse, if the plugin creates it's own thread that has an unhandled exception the host can't catch it and it fails. Is your code supposed to avoid this? If so, what might I be missing? – Brian Jan 04 '13 at 04:37
  • 1
    @Brian: no, this pattern isn't intended to manage exceptions. It's goal is to isolate plugins from a security perspective, preventing malicious 3rd party plugins from doing things that you as the host application developer would not want them to do. For your first example, I think it's quite reasonable that the host should handle exceptions, in the same way that it would handle exceptions when calling any other library method. However, I agree that the problem of exceptions on plugin-spawned threads is awkward. I don't see any obvious solution to that. Sorry. – Tim Coulter Jan 04 '13 at 11:39
  • 2
    I figured out that starting with .net2.0 an unhandled exception in any appdomain will bring down the whole process. To get around this you can add this to the app.config of your host: Then what I did was handle the AppDomain.UnhandledException event and unload the AppDomain if that event fires. Thanks! – Brian Jan 04 '13 at 18:24
  • With only appdomain isolaton it seems there is no other way than legacyUnhandledExceptionPolicy enabled="1" to prevent the main process from exiting. Another way could be in the System.AddIn.Hosting framework: AddInToken.Activate() "Activates an add-in in an external process, in a new application domain." http://msdn.microsoft.com/en-us/library/bb292114.aspx – Martin Meixger Jan 10 '13 at 18:00
  • What happens in `private IEnumerable Find()` if there are multiple versions of the same types in the `PluginPath`? ie: `MyPlugin.v1.0.dll` & `MyPlugin.v2.0.dll`. Will that throw an exception when trying to load the same types from v2 in the same `AppDomain`? Will the `TypeLocator` for v2 actually contain type info from v1? I'm also curious if it works the same when loading the assemblies from `byte[]` instead of physical file. I came across this Q/A because I need to support loading multiple versions, preferably from memory (db), and presumably cannot without separate app domains. – JoeBrockhaus Nov 26 '14 at 22:11
12

Because you're in different AppDomains, you can't just pass the instance across.

You'll need to make your plug-ins Remotable, and create a proxy in your main app. Have a look at the docs for CreateInstanceAndUnWrap, which has an example of how all this could work towards the bottom.

This is also another much broader overview by Jon Shemitz which I think is a good read. Good luck.

Alastair Maw
  • 5,373
  • 1
  • 38
  • 50
  • 2
    Thanks. I implemented something very similar to the CreateInstanceAndUnwrap code sample early in my experimentation and it indeed works. However, I found that when I extended the code to restrict the permissions of the AppDomain, it was unable to load my unsigned assembly. Clearly this approach is close to what I need but I am not clear how to put the final pieces in place. I will now read the broader overview that you recommended and hope that it leads me closer to the solution. Thanks again. – Tim Coulter Nov 10 '10 at 19:46
  • 1
    After some further experimentation I finally got this approach to work. It doesn't offer all the benfits of a MEF solution, but it is secure and also allows plugins to be unloaded. I am accepting your answer, but I will post a separate answer, detailing my approach, for the benefit of anyone else who is interested in this. Thanks again. – Tim Coulter Nov 11 '10 at 16:35
  • 1
    The overview link is dead – erotavlas Mar 27 '15 at 13:34
4

If you need your 3rd party extensions to load with a lower security privileges than the rest of your app, you should create a new AppDomain, create a MEF container for your extensions in that app domain, and then marshall calls from your application to the objects in the sandboxed app domain. The sandboxing occurs in how you create the app domain, it has nothing to to with MEF.

dthorpe
  • 35,318
  • 5
  • 75
  • 119
  • Thanks. This is presumably the approach suggested in one of the links in my question and would be by preferred solution. However, I tried to implement this but couldn't get it to work because the types in the extension assembly are not recognized by the deserialization process. Specifically, when I try to cast any of the plugin types to its known interface, it throws an exception. Do you perhaps have a code sample that you could share? Thanks again. – Tim Coulter Nov 10 '10 at 19:40
  • Don't have any code in this area, but I expect you will need to handle remoting between your app and extension domains separately from the MEF objects themselves. That is, I would compose the extensions in a MEF container with a collector class (with an ImportMany collection property for the extensions to show up in), then put a remoting wrapper around each extension instance. This will separate the AppDomain remoting from the extensions. You don't want the 3rd parties responsible for remoting. – dthorpe Nov 10 '10 at 21:15
  • I gave this idea my best shot and got quite close to a working solution, but I don't think it can be made to work properly because MEF itself requires file IO permissions in order to discover the assemblies that it will import from. So, although it is possible to put the MEF container in a sandbox, the permissions must be somewhat relaxed, which itself becomes a security risk. – Tim Coulter Nov 11 '10 at 16:31
1

Thanks for sharing with us the solution. I would like to make an important comment and a sugestion.

The comment is that you cannot 100% sandbox a plugin by loading it in a different AppDomain from the host. To find out, update DoSomethingDangerous to the following:

public override void DoSomethingDangerous()                               
{                               
    new Thread(new ThreadStart(() => File.ReadAllText(@"C:\Test.txt"))).Start();
}

An unhandled exception raised by a child thread can crash the whole application.

Read this for information concerning unhandle exceptions.

You can also read these two blog entries from the System.AddIn team that explain that 100% isolation can only be when the add-in is in a different process. They also have an example of what someone can do to get notifications from add-ins that fail to handle raised exceptions.

http://blogs.msdn.com/b/clraddins/archive/2007/05/01/using-appdomain-isolation-to-detect-add-in-failures-jesse-kaplan.aspx

http://blogs.msdn.com/b/clraddins/archive/2007/05/03/more-on-logging-unhandledexeptions-from-managed-add-ins-jesse-kaplan.aspx

Now the sugestion that I wanted to make has to do with the PluginFinder.FindPlugins method. Instead of loading each candidate assembly in a new AppDomain, reflecting on it's types and the unload the AppDomain, you could use Mono.Cecil. You then will not have to do any of this.

It is as simple as:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

foreach (TypeDefinition td in ad.MainModule.GetTypes())
{
    if (td.BaseType != null && td.BaseType.FullName == "MyNamespace.MyTypeName")
    {        
        return true;
    }
}

There are probably even better ways to do this with Cecil but I am not an expert user of this library.

Regards,

Panos Rontogiannis
  • 4,154
  • 1
  • 24
  • 29
0

An alternative would be to use this library: https://processdomain.codeplex.com/ It allows you running any .NET code in out-of-process AppDomain, which provides even better isolation, than the accepted answer. Of course one needs to choose a right tool for their task and in many cases the approach given in the accepted answer is all that is needed.

However if your are working with .net plugins that call into native libraries that may be unstable (the situation I personally came across) you want to run them not only in a separate app domain, but also in a separate process. A nice feature of this library is that it will automatically restarts the process if a plugin crashes it.

Andrew Savinykh
  • 25,351
  • 17
  • 103
  • 158
  • I'd be a bit horrified if `SecurityZone.Internet` allowed plugins to run native libraries - native code bypasses all .NET security mechanisms, so I assume it would get the same permissions as the user. Assuming .NET sandboxing isn't broken in this way, there are a couple other noteworthy reasons to run in a separate process: (1) StackOverflowException cannot be caught and will cross the AppDomain boundary, terminating the host process. (2) there's no way to limit memory allocation in an AppDomain... but you could watch a plugin's process for excess memory use and kill it. – Qwertie Nov 25 '19 at 19:47