Ищете практический подход к песочнице .NET плагинов - PullRequest
59 голосов
/ 10 ноября 2010

Я ищу простой и безопасный способ доступа к плагинам из приложения .NET. Хотя я представляю, что это очень распространенное требование, я изо всех сил пытаюсь найти что-то, что отвечает всем моим потребностям:

  • Хост-приложение обнаружит и загрузит свои сборки плагинов во время выполнения
  • Плагины будут создаваться неизвестными третьими лицами, поэтому они должны быть помещены в «песочницу», чтобы предотвратить выполнение вредоносного кода
  • Обычная сборка взаимодействия будет содержать типы, на которые ссылается как хост, так и его плагины
  • Каждая сборка плагина будет содержать один или несколько классов, которые реализуют общий интерфейс плагина
  • При инициализации экземпляра плагина хост передает ему ссылку на себя в виде интерфейса хоста
  • Хост будет вызывать плагин через его общий интерфейс, и плагины могут также вызывать хост
  • Хост и плагины будут обмениваться данными в форме типов, определенных в сборке взаимодействия (включая общие типы)

Я исследовал как MEF, так и MAF, но я изо всех сил пытаюсь понять, как любой из них может быть сделан в соответствии с требованиями.

Предполагая, что мое понимание правильное, MAF не может поддерживать передачу универсальных типов через границу изоляции, что важно для моего приложения. (MAF также очень сложен в реализации, но я был бы готов работать с этим, если бы мог решить проблему универсального типа).

MEF является почти идеальным решением, но, похоже, не соответствует требованиям безопасности, поскольку загружает свои сборки расширений в тот же домен приложений, что и хост, и, таким образом, по-видимому, предотвращает изолированную программную среду.

Я видел этот вопрос , который говорит о запуске MEF в режиме песочницы, но не описывает, как. В этом посте говорится, что «при использовании MEF вы должны доверять расширениям, чтобы не запускать вредоносный код, или предлагать защиту через Code Access Security», но, опять же, в нем не описывается, как. Наконец, есть этот пост , который описывает, как предотвратить загрузку неизвестных плагинов, но это не подходит для моей ситуации, так как даже легитимные плагины будут неизвестны.

Мне удалось применить атрибуты безопасности .NET 4.0 к моим сборкам, и MEF правильно их соблюдает, но я не понимаю, как это помогает мне блокировать вредоносный код, так как многие методы инфраструктуры, которые могут быть Угроза безопасности (например, методы System.IO.File) помечены как SecuritySafeCritical, что означает, что они доступны из SecurityTransparent сборок. Я что-то здесь упускаю? Есть ли какой-то дополнительный шаг, который я могу предпринять, чтобы сказать MEF, что он должен предоставлять интернет-привилегии сборкам плагинов?

Наконец, я также рассмотрел создание собственной простой архитектуры подключаемых модулей с песочницей, используя отдельный домен приложений, как описано здесь . Однако, насколько я вижу, этот метод позволяет мне использовать позднюю привязку только для вызова статических методов для классов в ненадежной сборке. Когда я пытаюсь расширить этот подход для создания экземпляра одного из моих классов плагинов, возвращаемый экземпляр не может быть приведен к общему интерфейсу плагина, что означает, что хост-приложение не может вызвать его. Есть ли какая-то техника, которую я могу использовать, чтобы получить строго типизированный прокси-доступ через границу AppDomain?

Я прошу прощения за длину этого вопроса; причина состояла в том, чтобы показать все пути, которые я уже исследовал, в надежде, что кто-то может предложить что-то новое, чтобы попробовать.

Большое спасибо за ваши идеи, Тим

Ответы [ 5 ]

52 голосов
/ 11 ноября 2010

Я принял ответ Аластера Мо, поскольку именно его предложение и ссылки привели меня к работоспособному решению, но я публикую здесь некоторые подробности того, что я сделал, для всех, кто пытается достичь чего-то подобного.

Напоминаем, что в простейшем виде мое приложение состоит из трех сборок:

  • Основная сборка приложения, которая будет потреблять плагины
  • Сборка взаимодействия, которая определяет общие типы, общие для приложения и его плагинов
  • Пример сборки плагина

Код ниже - это упрощенная версия моего реального кода, показывающая только то, что требуется для обнаружения и загрузки плагинов, каждый в своем собственном AppDomain:

Начиная с основной сборки приложения, основной класс программы использует служебный класс с именем PluginFinder для обнаружения подходящих типов плагинов в любых сборках в указанной папке плагинов. Для каждого из этих типов он затем создает экземпляр sandox AppDomain (с разрешениями зоны Интернета) и использует его для создания экземпляра обнаруженного типа плагина.

При создании 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();           
    }
}
12 голосов
/ 10 ноября 2010

Поскольку вы находитесь в разных доменах приложений, вы не можете просто передать экземпляр.

Вам нужно будет сделать ваши плагины удаленными и создать прокси в вашем основном приложении. Взгляните на документы для CreateInstanceAndUnWrap , в которых приведен пример того, как все это может работать в нижней части.

Это также еще один более широкий обзор Джона Шемитца , который я считаю хорошим чтением. Удачи.

4 голосов
/ 10 ноября 2010

Если вам нужно, чтобы ваши сторонние расширения загружались с более низкими привилегиями безопасности, чем остальная часть вашего приложения, вы должны создать новый домен AppDomain, создать контейнер MEF для ваших расширений в этом домене приложения, а затем выполнить маршалловые вызовы из вашего приложения. к объектам в изолированном домене приложения. Песочница происходит в том, как вы создаете домен приложения, она не имеет ничего общего с MEF.

1 голос
/ 04 апреля 2012

Спасибо, что поделились с нами решением.Я хотел бы сделать важный комментарий и предложение.

Комментарий заключается в том, что вы не можете на 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

Теперьsugestion, который я хотел сделать, связан с методом PluginFinder.FindPlugins.Вместо загрузки каждой сборки-кандидата в новый домен приложений с учетом его типов и выгрузки домена приложений вы можете использовать Mono.Cecil .Тогда вам не придется делать ничего из этого.

Это так же просто, как:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

foreach (TypeDefinition td in ad.MainModule.GetTypes())
{
    if (td.BaseType != null && td.BaseType.FullName == "MyNamespace.MyTypeName")
    {        
        return true;
    }
}

Возможно, есть даже лучшие способы сделать это с Сесилом, но я не опытный пользовательэтой библиотеки.

С уважением,

0 голосов
/ 01 мая 2013

Альтернативой может быть использование этой библиотеки: https://processdomain.codeplex.com/ Она позволяет запускать любой код .NET во внешнем AppDomain, что обеспечивает даже лучшую изоляцию, чем принятый ответ.Конечно, нужно выбрать правильный инструмент для их задачи, и во многих случаях подход, приведенный в принятом ответе, - это все, что нужно.

Однако, если вы работаете с плагинами .net, которые обращаются к собственным библиотекам, которыеможет быть нестабильным (ситуация, с которой я лично сталкивался), вы хотите запускать их не только в отдельном домене приложения, но и в отдельном процессе.Приятной особенностью этой библиотеки является то, что она автоматически перезапустит процесс, если плагин его завершит.

...