寻找一种实用的沙盒 .NET 插件方法
我正在寻找一种简单而安全的方法来从 .NET 应用程序访问插件。尽管我认为这是一个非常常见的要求,但我正在努力寻找满足我所有需求的任何东西:
- 主机应用程序将在运行时发现并加载其插件程序集
- 插件将由未知的第三方创建,因此必须将它们沙箱化以防止它们执行恶意代码
- 通用互操作程序集将包含由主机及其插件引用的类型
- 每个插件程序集将包含一个或多个实现通用插件接口的类
- 初始化插件实例时,主机将向其传递一个以主机接口的形式引用
- 自身主机将通过其公共接口调用插件,插件也可以同样调用主机
- 主机和插件将以互操作程序集中定义的类型的形式交换数据(包括通用类型)
我已经研究了 MEF 和 MAF,但我正在努力了解如何使它们中的任何一个能够满足要求。
假设我的理解是正确的,MAF 无法支持跨其隔离边界传递泛型类型,这对我的应用程序至关重要。 (MAF 的实现也非常复杂,但如果我能解决泛型类型问题,我将准备好使用它)。
MEF 几乎是一个完美的解决方案,但似乎达不到安全要求,因为它在与主机相同的 AppDomain 中加载其扩展程序集,因此显然阻止了沙箱。
我见过这个问题,它谈到在沙盒模式下运行MEF,但没有描述如何运行。 这篇文章指出“使用 MEF 时,您必须信任扩展不会运行恶意代码,或通过代码访问安全提供保护”,但同样,它没有描述如何进行。最后,有 这篇文章,描述了如何防止未知插件被加载,但这不适合我的情况,因为即使是合法的插件也会是未知的。
我已成功将 .NET 4.0 安全属性应用到我的程序集,并且 MEF 正确地遵守了它们,但我不知道这如何帮助我锁定恶意代码,因为许多框架方法可能构成安全威胁(例如 System.IO.File
的方法)被标记为 SecuritySafeCritical
,这意味着它们可以从 SecurityTransparent
程序集访问。我在这里错过了什么吗?我可以采取一些额外的步骤来告诉 MEF 它应该为插件程序集提供互联网权限吗?
最后,我还考虑使用单独的 AppDomain 创建自己的简单沙盒插件架构,如所述 这里。然而,据我所知,这种技术只允许我使用后期绑定来调用不受信任的程序集中的类的静态方法。当我尝试扩展此方法来创建我的插件类之一的实例时,返回的实例无法转换为公共插件接口,这意味着主机应用程序无法调用它。我可以使用某种技术来跨 AppDomain 边界进行强类型代理访问吗?
对于这个问题的长度,我深表歉意;原因是展示我已经研究过的所有途径,希望有人可以建议一些新的尝试。
非常感谢您的想法, 蒂姆
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
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(5)
我已经接受了 Alastair Maw 的回答,因为正是他的建议和链接引导我找到了可行的解决方案,但我在这里发布了我所做的一些详细信息,供其他可能试图实现类似目标的人参考。
提醒一下,在最简单的形式中,我的应用程序包含三个程序集:
下面的代码是我的真实代码的简化版本代码,仅显示发现和加载插件所需的内容,每个插件都在其自己的
AppDomain
中:从主应用程序程序集开始,主程序类使用名为
PluginFinder
的实用程序类发现指定插件文件夹中任何程序集中的合格插件类型。然后,对于每种类型,它都会创建一个 sandox AppDomain 实例(具有 Internet 区域权限),并使用它来创建已发现插件类型的实例。创建具有有限权限的
AppDomain
时,可以指定一个或多个不受这些权限约束的受信任程序集。为了在此处介绍的场景中实现此目的,必须对主应用程序程序集及其依赖项(互操作程序集)进行签名。对于每个加载的插件实例,可以通过其已知接口调用插件内的自定义方法,并且插件还可以通过其已知接口回调到主机应用程序。最后,主机应用程序卸载每个沙箱域。
在此示例代码中,主机应用程序类非常简单,仅公开一种可由插件调用的方法。但是,此类必须从
MarshalByRefObject
派生,以便可以在应用程序域之间引用它。PluginFinder
类只有一个公共方法,该方法返回已发现的插件类型的列表。此发现过程加载它找到的每个程序集,并使用反射来识别其合格类型。由于此过程可能会加载许多程序集(其中一些甚至不包含插件类型),因此它也在单独的应用程序域中执行,该应用程序域可能随后被卸载。请注意,由于上述原因,此类还继承了MarshalByRefObject
。由于Type
的实例可能无法在应用程序域之间传递,因此此发现过程使用名为TypeLocator
的自定义类型来存储每个已发现类型的字符串名称和程序集名称,这可能会导致然后安全地传回主应用程序域。互操作程序集包含将实现插件功能的类的基类(请注意,它也派生自
MarshalByRefObject
。此程序集还定义了
IHost
接口,使插件能够回调 。最后,每个插件都派生自互操作程序集中定义的基类,并实现其抽象方法。任何插件程序集中可能有多个继承类,并且可能有多个插件程序集
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.
因为您位于不同的 AppDomain 中,所以您不能只传递实例。
您需要使插件可远程连接,并在主应用程序中创建代理。查看 CreateInstanceAndUnWrap 的文档,其中有一个示例,说明所有这一切如何实现底部。
这也是另一个更广泛的 乔恩·谢米茨 (Jon Shemitz) 的概述,我认为这是一本很好的读物。祝你好运。
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.
如果您需要以比应用程序其余部分更低的安全权限加载第 3 方扩展,则应创建一个新的 AppDomain,在该应用程序域中为您的扩展创建一个 MEF 容器,然后将应用程序的调用编组到对象在沙盒应用程序域中。沙箱发生在您创建应用程序域的方式中,它与 MEF 无关。
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.
感谢您与我们分享解决方案。我想提出一个重要的评论和建议。
评论是,您不能通过将插件加载到与主机不同的 AppDomain 中来 100% 沙箱化插件。要找到答案,请将 DoSomethingDangerous 更新为以下内容:
子线程引发的未处理的异常可能会使整个应用程序崩溃。
请阅读本文,了解有关未处理异常的信息。
您还可以阅读 System.AddIn 团队的这两篇博客文章,其中解释了只有当加载项位于不同进程中时才能实现 100% 隔离。他们还提供了一个示例,说明如何从无法处理引发的异常的加载项中获取通知。
http://blogs.msdn.com/b/clraddins/archive/2007/05/01/using-appdomain-isolation-to-detect-add-in-failures-jesse-kaplan.aspx
现在我想要提出的建议与 PluginFinder.FindPlugins 方法有关。您可以使用 莫诺·塞西尔。然后您将不必执行任何这些操作。
它很简单:
使用 Cecil 可能有更好的方法来做到这一点,但我不是这个库的专家用户。
问候,
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,
另一种方法是使用此库: https://processdomain.codeplex.com/
它允许您在进程外 AppDomain 中运行任何 .NET 代码,这比接受的答案提供了更好的隔离。当然,人们需要为他们的任务选择一种正确的工具,并且在许多情况下,接受的答案中给出的方法就足够了。
但是,如果您使用的 .net 插件调用可能不稳定的本机库(我个人遇到的情况),您希望不仅在单独的应用程序域中运行它们,而且还希望在单独的过程。这个库的一个很好的功能是,如果插件崩溃它会自动重新启动进程。
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.