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
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.
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.
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:
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:
There are probably even better ways to do this with Cecil but I am not an expert user of this library.
Regards,
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.
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 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 sandoxAppDomain
(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.
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.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 inheritsMarshalByRefObject
for the reasons described above. Since instances ofType
may not be passed between application domains, this discovery process uses a custom type calledTypeLocator
to store the string name and assembly name of each discovered type, which may then be safely passed back to the main applicatin domain.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.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.