寻找一种实用的沙盒 .NET 插件方法
-
30-09-2019 - |
题
我正在寻找一种简单而安全的方法来从 .NET 应用程序访问插件。尽管我认为这是一个非常常见的要求,但我正在努力寻找满足我所有需求的任何东西:
- 主机应用程序将在运行时发现并加载其插件程序集
- 插件将由未知的第三方创建,因此必须将它们放入沙箱中以防止它们执行恶意代码
- 通用互操作程序集将包含主机及其插件引用的类型
- 每个插件程序集将包含一个或多个实现通用插件接口的类
- 初始化插件实例时,主机将以主机接口的形式向其传递对其自身的引用
- 主机将通过其公共接口调用插件,插件也可以同样调用主机
- 主机和插件将以互操作程序集中定义的类型(包括泛型类型)的形式交换数据
我已经研究了 MEF 和 MAF,但我正在努力了解如何使它们中的任何一个能够满足要求。
假设我的理解是正确的,MAF 无法支持跨其隔离边界传递泛型类型,这对我的应用程序至关重要。(MAF 的实现也非常复杂,但如果我能解决泛型类型问题,我将准备好使用它)。
MEF 几乎是一个完美的解决方案,但似乎达不到安全要求,因为它在与主机相同的 AppDomain 中加载其扩展程序集,因此显然阻止了沙箱。
我见过 这个问题, ,其中谈到在沙盒模式下运行 MEF,但没有描述如何运行。 这个帖子 声明“使用 MEF 时,您必须信任扩展不会运行恶意代码,或通过代码访问安全性提供保护”,但它同样没有描述如何操作。最后,还有 这个帖子, ,它描述了如何防止加载未知插件,但这不适合我的情况,因为即使是合法插件也是未知的。
我已成功将 .NET 4.0 安全属性应用到我的程序集,并且 MEF 正确地遵守了它们,但我不知道这如何帮助我锁定恶意代码,因为许多框架方法可能构成安全威胁(例如方法 System.IO.File
)被标记为 SecuritySafeCritical
, ,这意味着它们可以从 SecurityTransparent
组件。我在这里错过了什么吗?我可以采取一些额外的步骤来告诉 MEF 它应该为插件程序集提供互联网权限吗?
最后,我还考虑使用单独的 AppDomain 创建自己的简单沙盒插件架构,如下所述 这里. 。然而,据我所知,这种技术只允许我使用后期绑定来调用不受信任的程序集中的类的静态方法。当我尝试扩展此方法来创建我的插件类之一的实例时,返回的实例无法转换为公共插件接口,这意味着主机应用程序无法调用它。我可以使用某种技术来跨 AppDomain 边界进行强类型代理访问吗?
对于这个问题的长度,我深表歉意;原因是展示我已经研究过的所有途径,希望有人可以建议一些新的尝试。
非常感谢您的想法,蒂姆
解决方案
因为您在不同的应用程序中,所以您不能仅通过实例。
您需要使您的插件可远离,并在主应用程序中创建代理。看看文档 创建InstanceanDunWrap, ,其中有一个例子,说明所有这些都可以在底部工作。
这也是另一个更广泛的 乔恩·谢米茨(Jon Shemitz)的概述 我认为这是一本很好的读物。祝你好运。
其他提示
我已经接受了Alastair Maw的回答,因为正是他的建议和链接使我找到了一个可行的解决方案,但是我在这里发布了一些我所做的事情的一些细节,对于任何可能试图取得类似事物的人来说。
提醒一下,以最简单的形式,我的应用程序包括三个组件:
- 将消耗插件的主要应用程序组件
- 定义应用程序及其插件共享的常见类型的Interop组件
- 样品插件组件
下面的代码是我的真实代码的简化版本,仅显示发现和加载插件所需的内容 AppDomain
:
从主应用程序集开始,主程序类使用名为的实用程序类 PluginFinder
在指定的插件文件夹中的任何组件中发现合格的插件类型。对于这些类型中的每种 AppDomain
(具有Internet区域权限)并使用它来创建发现的插件类型的实例。
创建一个 AppDomain
在有限的权限下,可以指定一个或多个不受这些权限约束的值得信赖的集会。为了在此处介绍的方案中实现这一目标,必须签署主要应用程序组件及其依赖项(Interop组件)。
对于每个已加载的插件实例,可以通过其已知接口调用插件中的自定义方法,并且该插件还可以通过其已知接口回到主机应用程序。最后,主机应用程序将卸载每个沙盒域。
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
类只有一种公共方法,该方法返回发现的插件类型列表。此发现过程加载了它找到并使用反射来识别其合格类型的每个组件。由于此过程可能会加载许多组件(其中一些甚至不包含插件类型),因此在单独的应用程序域中也执行,该域可能会卸载。请注意,此类也继承 MarshalByRefObject
由于上述原因。由于实例 Type
可能不会在应用程序域之间传递,此发现过程使用称为的自定义类型 TypeLocator
要存储每种发现类型的字符串名称和汇编名称,然后可以安全地将其传递回主应用域。
/// <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; }
}
Interop组件包含将实现插件功能的类的基类(请注意,它也从 MarshalByRefObject
.
此组件还定义了 IHost
可以使插件回到主机应用程序的接口。
/// <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();
}
最后,每个插件源自Interop组件中定义的基类,并实现其抽象方法。任何插件组件中都可能有多个继承类,并且可能有多个插件组件。
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();
}
}
如果您需要第三方扩展名与应用程序的其余部分更低的安全特权加载,则应创建一个新的应用程序,为该应用程序域中的扩展程序创建一个MEF容器,然后将Marshall从应用程序调用到对象。在沙盒应用域中。沙箱发生在您创建应用程序域的方式中,与MEF无关。
感谢您与我们分享解决方案。我想发表重要的评论和sugention。
评论是,您无法通过将其加载到主机的不同应用程序中来100%沙盒插件。要找出答案,请更新DosomethingDangere,以下以下内容:
public override void DoSomethingDangerous()
{
new Thread(new ThreadStart(() => File.ReadAllText(@"C:\Test.txt"))).Start();
}
子线程提出的未经手的例外可能会使整个应用程序崩溃。
读 这 有关未手动例外的信息。
您还可以从系统中读取这两个博客条目。ADDIN团队解释说100%隔离只能在加载项处于不同的过程中。他们也有一个示例,说明某人可以从无法处理提出的例外的加载项中获取通知。
现在,我想做的是插件Findplugins方法。与其将每个候选组件加载到新的AppDomain中,反思其类型和卸载AppDomain,您可以使用 单核. 。然后,您将不必这样做。
它很简单:
AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);
foreach (TypeDefinition td in ad.MainModule.GetTypes())
{
if (td.BaseType != null && td.BaseType.FullName == "MyNamespace.MyTypeName")
{
return true;
}
}
与Cecil一起做这件事甚至更好,但我不是这个库的专家用户。
问候,
另一种选择是使用这个库: https://processdomain.codeplex.com/它允许您在进程外 AppDomain 中运行任何 .NET 代码,这比接受的答案提供了更好的隔离。当然,人们需要为他们的任务选择一种正确的工具,并且在许多情况下,接受的答案中给出的方法就足够了。
但是,如果您使用的 .net 插件调用可能不稳定的本机库(我个人遇到的情况),您 想 不仅在单独的应用程序域中运行它们,而且在单独的进程中运行它们。这个库的一个很好的功能是,如果插件崩溃它会自动重新启动进程。