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

时间:2010-11-10 14:51:53

标签: .net mef sandbox code-access-security maf

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

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

我已经调查了MEF和MAF,但我很难看到如何使它们中的任何一个符合要求。

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

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

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

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

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

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

非常感谢你的想法, 添

5 个答案:

答案 0 :(得分:52)

我已经接受了Alastair Maw的答案,因为正是他的建议和链接让我找到了一个可行的解决方案,但我在这里发布了一些关于我所做的具体细节,以及其他可能尝试实现类似内容的人。

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

  • 将使用插件的主要应用程序程序集
  • 一个互操作程序集,用于定义应用程序及其插件共享的常见类型
  • 示例插件程序集

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

从主应用程序程序集开始,主程序类使用名为PluginFinder的实用程序类来发现指定插件文件夹中任何程序集中的限定插件类型。对于每种类型,它然后创建一个sandox 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();           
    }
}

答案 1 :(得分:12)

因为您位于不同的AppDomains中,所以不能只是将实例传递过来。

您需要将插件设为Remotable,并在主应用中创建代理。请查看CreateInstanceAndUnWrap的文档,其中有一个示例,说明所有这些文档如何在底层工作。

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

答案 2 :(得分:4)

如果您需要使用比应用程序其他部分更低的安全权限加载第三方扩展程序,则应创建新的AppDomain,为该应用程序域中的扩展程序创建MEF容器,然后对应用程序中的调用进行编组到沙盒应用程序域中的对象。沙箱发生在您创建应用程序域的方式中,它与MEF没有任何关系。

答案 3 :(得分:1)

感谢您与我们分享解决方案。我想做一个重要的评论和消息。

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

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

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

阅读this以获取有关未处理例外情况的信息。

您还可以从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可能有更好的方法,但我不是这个库的专家用户。

此致

答案 4 :(得分:0)

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

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