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

Tim*_*ter 59 .net mef sandbox code-access-security maf

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

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

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

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

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

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

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

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

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

蒂姆,非常感谢你的想法

Tim*_*ter 52

我接受了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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在此示例代码中,宿主应用程序类非常简单,只暴露了一个可由插件调用的方法.但是,此类必须派生自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");
    }
}
Run Code Online (Sandbox Code Playgroud)

PluginFinder类只有一个返回发现插件类型的列表public方法.此发现过程加载它找到的每个程序集,并使用反射来标识其限定类型.由于此过程可能会加载许多程序集(其中一些甚至不包含插件类型),因此它也会在单独的应用程序域中执行,可以随后卸载.请注意,此类还会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; }
}
Run Code Online (Sandbox Code Playgroud)

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();
}
Run Code Online (Sandbox Code Playgroud)

最后,每个插件派生自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();           
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 这是一篇很棒的文章.很高兴你找到时间分享这一切.:) (5认同)
  • 我发现从.net2.0开始,任何appdomain中的未处理异常都会导致整个过程失败.要解决此问题,您可以将其添加到主机的app.config中:<runtime> <legacyUnhandledExceptionPolicy enabled ="1"/> </ runtime>然后我所做的是处理AppDomain.UnhandledException事件并卸载AppDomain事件火灾.谢谢! (2认同)

Ala*_*Maw 12

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

您需要将插件设置为Remotable,并在主应用程序中创建代理.看看CreateInstanceAndUnWrap的文档,其中有一个例子说明了所有这些对底层的作用.

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

  • 谢谢.我在实验的早期实现了与CreateInstanceAndUnwrap代码示例非常相似的东西,它确实有效.但是,我发现当我扩展代码以限制AppDomain的权限时,它无法加载我的无符号程序集.显然,这种方法接近我的需要,但我不清楚如何将最终的部分放在适当的位置.我现在将阅读您推荐的更广泛的概述,并希望它能让我更接近解决方案.再次感谢. (2认同)