Ищем практический подход к sandboxing.NET Плагины


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

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

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

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

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

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

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

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

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

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

5 59

5 ответов:

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

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

Это тоже гораздо шире обзор Джона Шемица который я думаю, что это хорошо читать. Удача.

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

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

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

приведенный ниже код является упрощенной версией моего реального кода, показывая только то, что требуется для обнаружения и загрузки плагинов, каждый в своем собственном 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");
    }
}

The 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();           
    }
}

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

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

комментарий заключается в том, что вы не можете на 100% изолировать плагин, загрузив его в другой домен приложения с хоста. Чтобы узнать это, обновите DoSomethingDangerous до следующего:

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

необработанное исключение, вызванное дочерним потоком, может привести к сбою всего приложения.

читать этой для получения информации, касающейся unhandle исключения.

вы также можете прочитать эти две записи из системы.Команда 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. Вместо загрузки каждой сборки-кандидата в новую 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;
    }
}

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

С уважением,

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

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