Finding a Practical Approach to Isolated .NET Plugins

I am looking for a simple and secure way to access plugins from a .NET application. Although I find this a very common requirement, I try my best to find everything that fits all my needs:

  • The host application will detect and load its plugin assemblies at run time
  • Plugins will be created by unknown third parties, so they must be isolated to prevent malicious code from executing them.
  • The general interop assembly will contain the types referenced by both the host and its plugins
  • Each plugin assembly will contain one or more classes that implement the common plugin interface
  • When the plugin instance is initialized, the host will give it a link to itself in the form of a host interface
  • The host will call the plugin through its common interface, and plugins can also call the host
  • The host and plugins will exchange data in the form of types defined in the interop assembly (including generic types)

I studied both MEF and MAF, but I'm struggling to figure out how any of them can be made to fit the bill.

Assuming my understanding is correct, MAF cannot support passing generic types across the isolation boundary, which is very important for my application. (MAF is also very difficult to implement, but I would be willing to work with this if I could solve the type problem).

MEF is an almost perfect solution, but doesn’t seem to meet the security requirements, since it loads its extension builds in the same AppDomain as the host, and thus seems to prevent the sandbox.

I saw this question , which talks about running MEF in isolated mode, but does not describe how. This message states that "when using MEF, you must trust the extensions so as not to run malicious code, or offer protection through access code protection", it does not describe how to do this. Finally, there is this post that describes how to prevent unknown plugins from loading, but this is not suitable for my situation, since even legitimate plugins will be unknown.

I managed to apply .NET 4.0 security attributes to my assemblies, and they are correctly followed by MEF, but I don’t see how this helps me to block malicious code, since many of the structure methods may be a security risk (for example, System.IO.File methods System.IO.File ) are marked as SecuritySafeCritical , which means that they are available from SecurityTransparent assemblies. Am I missing something? Is there any additional step I can take to tell MEF that it should provide internet privileges for building plugins?

Finally, I also looked at creating my own simple, isolated, plug-in architecture using a separate AppDomain, as described here . However, as far as I can see, this method allows me to use the last binding to call 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 ported to the general interface of the plugin, which means that the host application cannot call it. Is there any method that I can use to get strictly proxy access across the AppDomain border?

I apologize for the length of this question; the reason was to show all the paths that I have already explored, in the hope that someone can offer something new to try.

Thanks so much for your ideas, Tim

+59
mef sandbox code-access-security maf
Nov 10 2018-10-10
source share
5 answers

Since you are in different AppDomains, you cannot just pass the instance through.

You will need to make your Remotable plugins and create a proxy server in the main application. Take a look at the docs for CreateInstanceAndUnWrap , which has an example of how all this can work at the bottom.

This is another far more general overview of Jon Shemitz , which I think is well read. Good luck.

+12
Nov 10 2018-10-10
source share
β€” -

I 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 about what I did for anyone who might be trying to achieve something similar.

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

  • The main assembly of the application, which will consume plugins
  • A gateway that defines common types shared by an application and its plugins
  • Plugin assembly example

The code below is a simplified version of my real code, showing only what it takes to detect and download plugins, each in its own AppDomain :

Starting with the main assembly of the application, the main class of the program uses a utility class called PluginFinder to detect high-quality types of plug-ins in any assemblies in a specific folder of the plug-in. For each of these types, it creates an instance of sandox AppDomain (with Internet zone permissions) and uses it to create an instance of the detected plugin type.

When creating an AppDomain with limited permissions, you can specify one or more trusted assemblies that do not fall under these permissions. To accomplish this in the scenario presented here, the main assembly of the application and its dependencies (interaction assembly) must be signed.

For each loaded instance of the plugin, user methods in the plugin can be called through its well-known interface, and the plugin can also call back to the host application through its well-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 code example, the host application class is very simple, exposing only one method that can be called by plugins. However, this class must be obtained 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, which returns a list of detected plugin types. This discovery process loads each assembled assembly and uses reflection to identify its qualification types. Since this process can potentially load many assemblies (some of which do not even contain plug-in types), it also runs in a separate application domain, which can subsequently be unloaded. Note that this class also inherits MarshalByRefObject for the reasons described above. Because Type instances cannot be transferred between application domains, this discovery process uses a custom type called TypeLocator to store the string name and assembly name for each detected type, which can then be safely transferred back to the main application 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 a base class for classes that will implement the functionality of the plugin (note that it also comes from MarshalByRefObject .

This assembly also defines the IHost interface, which allows plugins to go to the main 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 is derived from the base class defined in the interop assembly and implements its abstract methods. In any plug-in assembly, there may be several inheriting classes, and there may be several plug-in builders.

 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(); } } 
+51
Nov 11 2018-10-11
source share

If you want your third-party extensions to load with lower security rights than other applications, you must create a new AppDomain, create an MEF container for your extensions in this application domain, and then call calls from the application to objects in the isolated application domain. The sandbox happens in the way you create the application domain, and has nothing to do with MEF.

+4
Nov 10 '10 at 18:23
source share

Thank you for sharing the solution with us. I would like to make an important comment and sentence.

The comment is that you cannot 100% isolate the plugin by loading it into another AppDomain from the host. To find out, upgrade DoSomethingDangerous to the following:

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

An unhandled exception thrown by a child thread can cause the entire application to crash.

Read this for exclusive exceptions.

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

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 I wanted to do is linked to the PluginFinder.FindPlugins method. Instead of loading every candidate assembly in the new AppDomain, reflecting its types and unloading AppDomain, you can use Mono.Cecil . You then do not have to do anything.

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 may be even better ways to do this with Cecil, but I am not an expert in this library.

Hello,

+1
Apr 04 '12 at 9:16
source share

An alternative could be to use this library: https://processdomain.codeplex.com/ It allows you to run any .NET code in the embedded AppDomain, which provides even better isolation than the accepted answer. Of course, you need to choose the right tool for your task, and in many cases the approach given in the accepted answer is all that is needed.

However, if you work with .net plugins that call into native libraries, which may be unstable (a situation that I have personally encountered), you want to run them not only in a separate application domain, but also in a separate process. A good feature of this library is that it will automatically restart the process if the plugin works.

0
Apr 30 '13 at 23:38
source share



All Articles