寻找沙盒.NET插件的实用方法

我正在寻找一种简单而安全的方式来访问.NET应用程序的插件。 虽然我想这是一个非常普遍的要求,但我正在努力寻找符合我所有需求的任何东西:

  • 主机应用程序将在运行时发现并加载其插件程序集
  • 插件将由未知的第三方创build,所以他们必须被沙箱化,以防止他们执行恶意代码
  • 一个常见的互操作程序集将包含主机及其插件引用的types
  • 每个插件程序集将包含一个或多个实现通用插件接口的类
  • 当初始化一个插件实例时,主机会以主机接口的forms传递一个对自身的引用
  • 主机将通过其通用接口调用插件,插件可能会调用主机
  • 主机和插件将以互操作程序集中定义的types(包括generics)交换数据,

我已经调查了MEF和MAF,但是我正在努力研究如何使它们中的任何一个适合这个法案。

假设我的理解是正确的,那么MAF就不能支持跨越它的隔离边界的genericstypes的传递,这对我的应用是非常重要的。 (MAF的实现也非常复杂,但是如果我能解决genericstypes问题,我会准备好解决这个问题)。

MEF几乎是一个完美的解决scheme,但似乎不符合安全要求,因为它将扩展程序集加载到与主机相同的AppDomain中,因此显然可以防止沙盒。

我已经看到了这个问题 ,它谈到了在沙箱模式下运行MEF,但没有描述如何。 这篇文章指出:“使用MEF时,您必须信任扩展程序不运行恶意代码,或者通过代码访问安全性提供保护”,但是,它并没有描述如何。 最后,有一篇文章介绍了如何防止未知的插件被加载,但这并不适合我的情况,因为即使是合法的插件也是未知的。

我已经成功地将.NET 4.0安全属性应用到我的程序集中,并被MEF正确地重视,但是我不明白这是如何帮助我locking恶意代码的,因为许多框架方法可能是安全威胁如System.IO.File方法)被标记为SecuritySafeCritical ,这意味着它们可以从SecurityTransparent程序集中访问。 我在这里错过了什么? 是否有一些额外的步骤可以告诉MEF它应该为插件程序集提供互联网特权?

最后,我还看到了使用单独的AppDomain创build自己的简单沙盒插件体系结构,如此处所述。 但是,据我所见,这种技术只允许我使用后期绑定来调用不可信程序集中类的静态方法。 当我试图扩展这种方法来创build我的一个插件类的实例时,返回的实例不能被转换为通用的插件接口,这意味着主机应用程序不可能调用它。 是否有一些技术可以用来跨AppDomain边界获得强types的代理访问?

我对这个问题的长度表示歉意。 原因是要展示我已经调查过的所有途径,希望有人能提出一些新的尝试。

Tim非常感谢你的想法

由于您处于不同的AppDomain中,因此不能仅将该实例传递给对象。

您将需要使您的插件远程,并在您的主应用程序中创build一个代理。 看看CreateInstanceAndUnWrap的文档,它有一个如何所有这些都可以在底部工作的例子。

这也是Jon Shemitz的另一个更广泛的概述 ,我认为这是一个很好的阅读。 祝你好运。

我已经接受了Alastair Maw的回答,因为这是他的build议和联系,使我得到了一个可行的解决scheme,但是我在这里公布了一些我正在做的事情的细节,对于可能试图实现类似事情的任何人。

提醒一下,我的应用程序最简单的forms包含三个程序集:

  • 主要的应用程序组件将消耗插件
  • 互操作程序集,定义应用程序及其插件共享的公共types
  • 一个示例插件程序集

下面的代码是我真实代码的简化版本,仅显示发现和加载插件所需的内容,每个插件都在其自己的AppDomain

从主应用程序集开始,主程序类使用名为PluginFinder的实用程序类来发现指定插件文件夹中任何程序集内的PluginFinder插件types。 对于每个types,它然后创build一个sandox AppDomain的实例(具有Internet区域权限),并使用它创build发现的插件types的实例。

在创build具有有限权限的AppDomain时,可以指定一个或多个不受这些权限约束的受信任程序集。 为了在这里介绍的场景中完成这个任务,主应用程序集合及其依赖关系(interop程序集)必须被签名。

对于每个加载的插件实例,插件中的自定义方法可以通过其已知的接口调用,插件也可以通过其已知的接口callback主机应用程序。 最后,主机应用程序卸载每个沙箱域。

 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); } } 

在这个示例代码中,宿主应用程序类非常简单,只是提供了一个可能被插件调用的方法。 但是,该类必须从MarshalByRefObject派生,以便可以在应用程序域之间引用它。

 /// <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"); } } 

PluginFinder类只有一个公共方法返回发现的插件types列表。 此发现过程加载它find的每个程序集,并使用reflection来识别其限定types。 由于此进程可能会加载许多程序集(其中一些程序集甚至不包含插件types),因此它也会在单独的应用程序域中执行,可能会被轻率卸载。 请注意,由于上述原因,此类还inheritanceMarshalByRefObject 。 由于Type实例不能在应用程序域之间传递,因此这个发现过程使用一个名为TypeLocator的自定义types来存储每个发现types的string名称和程序集名称,然后可以安全地将其传回到主应用程序域。

 /// <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; } } 

互操作程序集包含将实现插件function的类的基类(请注意,它也来自MarshalByRefObject

这个程序集还定义了IHost接口,使得插件可以callback到主机应用程序。

 /// <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(); } 

最后,每个插件派生自互操作程序集中定义的基类并实现其抽象方法。 在任何插件程序集中都可能有多个inheritance类,并且可能有多个插件程序集。

 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(); } } 

如果您需要使用比其他应用程序更低的安全权限加载第三方扩展,则应该创build一个新的AppDomain,在该应用程序域中为您的扩展创build一个MEF容器,然后将您的应用程序调用到对象在沙盒应用程序域。 sandboxing发生在你如何创build应用程序域,它与MEF没有任何关系。

感谢您与我们分享解决scheme。 我想提出一个重要的评论和一个消解。

评论是,你不能100%沙箱插件通过加载在一个不同的AppDomain从主机。 为了找出答案,请将DoSomethingDangerous更新为以下内容:

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

子线程引发的未处理exception可能会导致整个应用程序崩溃。

阅读有关未处理exception的信息。

您还可以从System.AddIn团队中阅读这两个博客条目,解释只有当加载项处于不同的进程时,才能进行100%隔离。 他们还有一个示例,说明某人可以通过加载项获取通知,但无法处理引发的exception。

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

现在我想要做的消解与PluginFinder.FindPlugins方法有关。 不是将每个候选程序集加载到新的AppDomain中,而是反映它的types并卸载AppDomain,可以使用Mono.Cecil 。 那么你将不必做任何这个。

这是如此简单:

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

有可能更好的方法来与塞西尔,但我不是这个图书馆的专家用户。

问候,

另一种方法是使用这个库: https : //processdomain.codeplex.com/它允许你在进程外的AppDomain中运行任何.NET代码,这比被接受的答案提供了更好的隔离。 当然,我们需要为他们的任务select一个正确的工具,在许多情况下,接受答案中给出的方法就是所需要的。

但是,如果您正在使用调用本地库的.net插件,而这些插件可能不稳定(我个人遇到这种情况),您不仅要在单独的应用程序域中运行它们,还要在单独的过程中运行它们。 这个库的一个很好的function是,如果一个插件崩溃,它会自动重启进程。