60

我正在寻找一种从 .NET 应用程序访问插件的简单且安全的方法。虽然我认为这是一个非常普遍的要求,但我很难找到满足我所有需求的任何东西:

  • 宿主应用程序将在运行时发现并加载其插件程序集
  • 插件将由未知的 3rd 方创建,因此必须对它们进行沙盒处理以防止它们执行恶意代码
  • 一个通用的互操作程序集将包含主机及其插件引用的类型
  • 每个插件程序集将包含一个或多个实现通用插件接口的类
  • 初始化插件实例时,宿主会以宿主接口的形式传递给它自己的引用
  • 主机将通过其通用接口调用插件,插件也可以调用主机
  • 主机和插件将以互操作程序集中定义的类型(包括泛型)的形式交换数据

我对 MEF 和 MAF 都进行了调查,但我很难了解如何使它们中的任何一个符合要求。

假设我的理解是正确的,MAF 无法支持泛型类型跨越其隔离边界的传递,这对我的应用程序至关重要。(MAF 实现起来也非常复杂,但如果我能解决泛型类型问题,我会准备好使用它)。

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

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

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

最后,我还研究了使用单独的 AppDomain 创建我自己的简单沙盒插件架构,如此所述。然而,据我所知,这种技术只允许我使用后期绑定来调用不受信任程序集中类的静态方法。当我尝试扩展此方法以创建我的插件类之一的实例时,返回的实例无法转换为通用插件接口,这意味着主机应用程序无法调用它。是否有一些技术可以用来跨 AppDomain 边界获取强类型代理访问?

我为这个问题的长度道歉;原因是展示我已经调查过的所有途径,希望有人可以提出一些新的尝试。

非常感谢你的想法,蒂姆

4

5 回答 5

55

我已经接受了 Alastair Maw 的回答,因为正是他的建议和链接让我找到了一个可行的解决方案,但我在这里发布了一些关于我所做的事情的详细信息,供其他可能试图实现类似目标的人使用。

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

  • 将使用插件的主应用程序程序集
  • 定义应用程序及其插件共享的通用类型的互操作程序集
  • 示例插件程序集

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

从主应用程序程序集开始,主程序类使用一个名为的实用程序类PluginFinder来发现指定插件文件夹中任何程序集内的合格插件类型。对于这些类型中的每一个,它会创建一个 Sandox 实例AppDomain(具有 Internet 区域权限)并使用它来创建一个已发现插件类型的实例。

创建AppDomain具有有限权限的程序集时,可以指定一个或多个不受这些权限约束的受信任程序集。要在此处介绍的场景中完成此操作,必须对主应用程序程序集及其依赖项(互操作程序集)进行签名。

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

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

互操作程序集包含将实现插件功能的类的基类(请注意,它也派生自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();
}

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

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();           
    }
}
于 2010-11-11T17:24:13.417 回答
12

因为您在不同的 AppDomains 中,所以您不能只传递实例。

您需要使您的插件可远程处理,并在您的主应用程序中创建一个代理。查看CreateInstanceAndUnWrap的文档,其中有一个示例,说明所有这些如何在底部起作用。

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

于 2010-11-10T18:19:44.877 回答
4

如果您需要以低于应用程序其余部分的安全权限加载第 3 方扩展程序,您应该创建一个新的 AppDomain,在该应用程序域中为您的扩展程序创建一个 MEF 容器,然后将应用程序中的调用编组到对象在沙盒应用程序域中。沙盒发生在您创建应用程序域的方式中,它与 MEF 无关。

于 2010-11-10T18:23:49.513 回答
1

感谢您与我们分享解决方案。我想提出重要的意见和建议。

评论是您不能通过将插件加载到与主机不同的 AppDomain 中来 100% 沙箱化插件。要找出答案,请将 DoSomethingDangerous 更新为以下内容:

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

子线程引发的未处理异常可能会使整个应用程序崩溃。

阅读本文以获取有关未处理异常的信息。

您还可以阅读 System.AddIn 团队的这两篇博客文章,其中解释了 100% 隔离只能在加载项处于不同进程时进行。他们还提供了一个示例,说明如何从无法处理引发异常的加载项获取通知。

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 方法有关。您可以使用Mono.Cecil,而不是将每个候选程序集加载到新的 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 可能有更好的方法来做到这一点,但我不是这个库的专家用户。

问候,

于 2012-04-04T09:16:33.243 回答
0

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

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

于 2013-04-30T23:38:00.877 回答