Szukasz praktycznego podejścia do sandboxing.NET wtyczki

Szukam prostego i bezpiecznego sposobu dostępu do wtyczek z aplikacji. NET. Chociaż wyobrażam sobie, że jest to bardzo powszechny wymóg, staram się znaleźć wszystko, co spełni wszystkie moje potrzeby: {]}

  • aplikacja hosta odkryje i załaduje swoje zespoły wtyczek w czasie wykonywania
  • Wtyczki będą tworzone przez nieznane strony trzecie, więc muszą być piaskownicy, aby uniemożliwić im wykonywanie złośliwego kodu
  • wspólny zespół interop zawiera typy, które są odwołuje się zarówno do hosta, jak i jego wtyczek
  • Każdy zespół wtyczek będzie zawierał jedną lub więcej klas, które implementują wspólny interfejs wtyczki
  • podczas inicjalizacji wystąpienia wtyczki, host przekaże mu odniesienie do siebie w postaci interfejsu hosta
  • host wywoła się do wtyczki za pośrednictwem wspólnego interfejsu, a wtyczki mogą również wywoływać do hosta
  • host i Wtyczki będą wymieniać dane w postaci typów zdefiniowanych w interop assembly (including generic types)

Badałem zarówno MEF, jak i MAF, ale staram się zobaczyć, jak można je dopasować do ustawy.

Zakładając, że moje zrozumienie jest poprawne, MAF nie jest w stanie poprzeć przechodzenia typów generycznych przez granicę izolacji, co jest niezbędne dla mojej aplikacji. (MAF jest również bardzo złożony w implementacji, ale byłbym przygotowany do pracy z tym, gdybym mógł rozwiązać problem typu generycznego).

MEF jest prawie idealny rozwiązanie, ale wydaje się, że nie spełnia wymagań bezpieczeństwa, ponieważ ładuje swoje zespoły rozszerzeń w tej samej AppDomain co host, a tym samym najwyraźniej zapobiega piaskownicy.

Widziałem to pytanie, które mówi o uruchomieniu MEF w trybie piaskownicy, ale nie opisuje jak. ten post stwierdza, że "podczas korzystania z MEF musisz ufać rozszerzeniom, aby nie uruchamiały złośliwego kodu lub nie oferowały ochrony za pomocą zabezpieczeń dostępu do kodu", ale ponownie, nie opisuje w jaki sposób. Wreszcie, jest ten post, który opisuje, jak zapobiec ładowaniu nieznanych wtyczek, ale nie jest to odpowiednie dla mojej sytuacji, ponieważ nawet legalne wtyczki będą nieznane.

Udało mi się zastosować atrybuty bezpieczeństwa. NET 4.0 do moich zestawów i są one prawidłowo przestrzegane przez MEF, ale nie widzę, jak to pomaga mi zablokować złośliwy kod, ponieważ wiele metod frameworku, które mogą stanowić zagrożenie dla bezpieczeństwa (takich jak metody System.IO.File) są oznaczone jako SecuritySafeCritical, które oznacza, że są one dostępne z SecurityTransparent zespołów. Coś mnie ominęło? Czy jest jakiś dodatkowy krok, który mogę podjąć, aby powiedzieć MEF, że powinien zapewniać przywileje internetowe zespołom wtyczek?

Wreszcie, przyjrzałem się również stworzeniu własnej architektury wtyczek typu sandbox, używając oddzielnego AppDomain, jak opisano tutaj . Jednak, o ile widzę, ta technika pozwala mi używać późnego wiązania do wywoływania statycznych metod na klasach w niezaufanym montaż. Kiedy staram się rozszerzyć to podejście do tworzenia wystąpienia jednej z moich klas plugin, zwracane wystąpienie nie może być oddane do wspólnego interfejsu plugin, co oznacza, że jest to niemożliwe dla aplikacji hosta do wywołania do niego. Czy jest jakaś technika, której mogę użyć, aby uzyskać silnie wpisany dostęp proxy przez granicę AppDomain?

Przepraszam za długość tego pytania; powodem było pokazanie wszystkich dróg, które już zbadałem, w nadziei, że ktoś może zaproponować coś nowego do wypróbowania.

Wielkie dzięki za pomysły, Tim

Author: Community, 2010-11-10

5 answers

Ponieważ jesteś w różnych AppDomains, nie możesz po prostu przekazać instancji.

Musisz zdalnie skonfigurować wtyczki i utworzyć serwer proxy w głównej aplikacji. Spójrz na dokumenty dla CreateInstanceAndUnWrap , który ma przykład, jak to wszystko może działać w kierunku dołu.

Jest to również kolejny znacznie szerszy przegląd autorstwa Jona Shemitza , który uważam za dobrą lekturę. Powodzenia.

 12
Author: Alastair Maw,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-10 18:19:44

Zaakceptowałem odpowiedź Alastaira Mawa, ponieważ to jego sugestia i linki doprowadziły mnie do wykonalnego rozwiązania, ale zamieszczam tutaj kilka szczegółów dokładnie tego, co zrobiłem, dla każdego, kto może próbować osiągnąć coś podobnego.

Dla przypomnienia, w najprostszej formie moje zgłoszenie składa się z trzech zespołów:

  • główny zespół aplikacji, który będzie zużywał wtyczki
  • zespół interop, który definiuje wspólne typy współdzielone przez aplikację i its plugins
  • przykładowy zestaw wtyczek

Poniższy kod jest uproszczoną wersją mojego prawdziwego kodu, pokazującą tylko to, co jest wymagane do odkrywania i ładowania wtyczek, każda w swoim własnym AppDomain: {]}

Zaczynając od głównego zestawu aplikacji, główna klasa programu używa klasy użytkowej o nazwie PluginFinder, aby odkryć kwalifikujące typy wtyczek w dowolnych zestawach w wyznaczonym folderze wtyczek. Dla każdego z tych typów tworzy instancję sandox AppDomain (z Internetem uprawnienia strefowe) i używa go do tworzenia instancji typu wykrytej wtyczki.

Podczas tworzenia AppDomain z ograniczonymi uprawnieniami można określić jeden lub więcej zaufanych zestawów, które nie podlegają tym uprawnieniom. Aby to osiągnąć w przedstawionym scenariuszu, należy podpisać główny zespół aplikacji i jego zależności (zespół interop).

Dla każdej załadowanej instancji wtyczki, niestandardowe metody wewnątrz wtyczki mogą być wywoływane poprzez jej znane interfejs i wtyczka mogą również oddzwonić do aplikacji hosta za pośrednictwem znanego interfejsu. Wreszcie, aplikacja hosta rozładowuje każdą z domen piaskownicy.

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

W tym przykładowym kodzie, Klasa aplikacji hosta jest bardzo prosta, ujawniając tylko jedną metodę, która może być wywołana przez wtyczki. Jednak klasa ta musi wywodzić się z MarshalByRefObject, aby mogła być odwoływana między domenami aplikacji.

/// <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");
    }
}

Klasa PluginFinder ma tylko jedną publiczną metodę, która zwraca listę odkrytych typy wtyczek. Ten proces wykrywania ładuje każdy znaleziony zespół i wykorzystuje odbicie do identyfikacji jego kwalifikujących się typów. Ponieważ proces ten może potencjalnie załadować wiele zestawów (niektóre z nich nie zawierają nawet typów wtyczek), jest on również wykonywany w oddzielnej domenie aplikacji, która może być poddźwiękowo rozładowana. Zauważ, że ta klasa dziedziczy również MarshalByRefObject z powodów opisanych powyżej. Ponieważ instancje Type mogą nie być przekazywane między domenami aplikacji, proces ten wykorzystuje niestandardowy typ o nazwie TypeLocator do przechowywania nazwy łańcucha znaków i nazwy złożenia każdego odkrytego typu, który może być bezpiecznie przekazywany z powrotem do głównej domeny aplikacji.

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

Zgromadzenie interop zawiera klasę bazową dla klas, które będą implementować funkcjonalność wtyczki (zauważ, że wywodzi się również z MarshalByRefObject.

Ten zestaw definiuje również interfejs IHost, który umożliwia pluginom wywołanie z powrotem do aplikacji hosta.

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

Wreszcie, każda wtyczka pochodzi z klasa bazowa zdefiniowana w zbiorze interop i implementuje jej metody abstrakcyjne. W każdym zestawie wtyczek może być wiele klas dziedziczących i może być wiele zestawów wtyczek.

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();           
    }
}
 51
Author: Tim Coulter,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2011-12-29 23:52:47

Jeśli potrzebujesz rozszerzeń innych firm do ładowania z niższymi uprawnieniami bezpieczeństwa niż reszta aplikacji, powinieneś utworzyć nową AppDomain, utworzyć kontener MEF dla rozszerzeń w tej domenie aplikacji, a następnie wywołać marshall z aplikacji do obiektów w piaskownicy domeny aplikacji. Sandboxing występuje w sposobie tworzenia domeny aplikacji, nie ma to nic wspólnego z MEF.

 4
Author: dthorpe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2010-11-10 18:23:49

Dziękujemy za podzielenie się z nami rozwiązaniem. Chciałbym przedstawić ważny komentarz i sugestię.

Komentarz jest taki, że nie można w 100% sandbox wtyczki ładując go w innej AppDomain od hosta. Aby się dowiedzieć, zaktualizuj DoSomethingDangerous do następującego:

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

Nieobsługiwany wyjątek wywołany przez wątek potomny może spowodować awarię całej aplikacji.

Przeczytaj to , aby uzyskać informacje dotyczące nieobsługiwanych wyjątków.

Możesz również przeczytać te dwa wpisy na blogu z systemu.Zespół AddIn, który wyjaśnia, że 100% izolacja może być tylko wtedy, gdy dodatek jest w innym procesie. Mają również przykład, co ktoś może zrobić, aby otrzymywać powiadomienia z dodatków, które nie obsługują podniesione wyjątki.

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

Teraz sugestia, którą chciałem zrobić, ma związek z wtyczką.Metoda FindPlugins. Zamiast ładować każdy zespół kandydata do nowej domeny AppDomain, zastanawiając się nad jego typami i rozładować AppDomain, można użyć Mono.Cecil . Wtedy nie będziesz musiał nic z tego robić.

To jest tak proste jak:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

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

Są prawdopodobnie jeszcze lepsze sposoby, aby to zrobić z Cecilem, ale nie jestem ekspertem w tej bibliotece.

Pozdrawiam,

 1
Author: Panos Rontogiannis,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-04 09:16:33

Alternatywą byłoby użycie tej biblioteki: https://processdomain.codeplex.com/ Pozwala na uruchamianie dowolnego kodu. NET W out-of-process AppDomain, co zapewnia jeszcze lepszą izolację, niż zaakceptowana odpowiedź. Oczywiście trzeba wybrać odpowiednie narzędzie do swojego zadania i w wielu przypadkach podejście podane w zaakceptowanej odpowiedzi jest wszystkim, co jest potrzebne.

Jeśli jednak pracujesz z wtyczkami. Net, które wywołują do natywnych bibliotek, które mogą być niestabilne (sytuacja I osobiście natknąłem się) chcesz , Aby uruchamiać je nie tylko w oddzielnej domenie aplikacji, ale także w oddzielnym procesie. Miłą cechą tej biblioteki jest to, że automatycznie uruchomi proces, jeśli wtyczka go zawiesi.

 0
Author: Andrew Savinykh,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-04-30 23:38:00