59 votes

La recherche d'une approche pratique pour le bac à sable .NET plugins

Je suis à la recherche d'une façon simple et sécurisée pour accéder à des plugins à partir de un .NET application. Mais j'imagine que c'est une très exigence commune, j'ai du mal à trouver quelque chose qui répond à tous mes besoins:

  • L'application hôte permettra de découvrir et de charger le plugin assemblées au moment de l'exécution
  • Les Plugins seront créés par des inconnus 3e parties, de sorte qu'ils doivent être en bac à sable pour empêcher l'exécution de code malveillant
  • D'un commun assembly interop va contenir des types qui sont référencés par à la fois l'hôte et de ses plugins
  • Chaque plugin assembly contient une ou plusieurs classes qui implémentent une commune interface du plugin
  • Lors de l'initialisation d'un plugin d'exemple, l'hôte va passer une référence à lui-même sous la forme d'une interface de l'hôte
  • L'hôte va l'appeler le plugin via son interface commune et les plugins peuvent appeler l'hôte de la même manière
  • L'hôte et les plugins seront échanger des données sous la forme de types définis dans l'assembly interop (y compris les types génériques)

J'ai étudié à la fois la MEF et la MAF, mais j'ai du mal à voir comment l'une d'elles peuvent être faites pour s'adapter à la facture.

En supposant que ma compréhension est correcte, le CRG est incapable de supporter le passage de types génériques dans l'ensemble de son isolement limite, ce qui est essentiel à ma demande. (CRG est aussi très complexe à mettre en œuvre, mais je serais prêt à travailler avec si je pouvais résoudre le générique type de problème).

MEF est presque une solution parfaite, mais semble tomber à court de l'exigence en matière de sécurité, comme il charge son extension assemblées dans le même domaine d'application que l'hôte, et donc apparemment empêche bac à sable.

J'ai vu cette question, qui parle de l'exécution de la MEF dans un mode bac à sable, mais ne décrit pas comment. Ce post précise que "lors de l'utilisation de la MEF vous devez faire confiance à des extensions de ne pas exécuter du code malveillant, ou offrir une protection par Code d'Accès de Sécurité", mais, encore une fois, il ne décrit pas comment. Enfin, il y a ce postqui explique comment empêcher inconnu plugins d'être chargé, mais ce n'est pas approprié à ma situation, que même légitime plugins sera inconnu.

J'ai réussi à appliquer .NET 4.0 attributs de sécurité à mon assemblées et qu'ils sont correctement respectés par le MEF, mais je ne vois pas comment cela m'aide à verrouiller malicous code, autant de du cadre les méthodes qui pourraient être une menace pour la sécurité (tels que les méthodes d' System.IO.File) sont marqués en tant que SecuritySafeCritical, ce qui signifie qu'ils sont accessibles à partir de SecurityTransparent des assemblages. Suis-je manqué quelque chose? Est-il une étape supplémentaire que je peux dire MEF qu'il devrait fournir des privilèges d'internet de plugin assemblées?

Enfin, j'ai aussi regardé la création de mon propre simple bac à sable d'une architecture de plugin, dans un autre domaine d'application, comme décrit ici. Cependant, aussi loin que je peux voir, cette technique me permet d'utiliser la liaison tardive pour invoquer les méthodes statiques de classes dans la confiance de l'assemblée. Lorsque j'essaie d'étendre cette approche pour créer une instance d'une de mes classes de plugin, le retour de l'instance ne peut pas être jeté à la commune interface du plugin, ce qui signifie qu'il est impossible pour l'application hôte pour l'appeler. Est-il une technique que je peux utiliser pour obtenir fortement typées proxy d'accès dans le domaine d'application des limites?

Je m'excuse pour la longueur de cette question; la raison était de montrer toutes les possibilités que j'ai déjà étudié, dans l'espoir que quelqu'un puisse proposer quelque chose de nouveau à essayer.

Merci beaucoup pour vos idées, Tim

53voto

Tim Coulter Points 2748

J'ai accepté Alastair Maw réponse, comme c'était sa suggestion, et les liens qui m'a conduit à une solution viable, mais je suis de poster ici quelques détails de ce que j'ai fait, pour toute personne d'autre qui peut être essayer de réaliser quelque chose de similaire.

Pour rappel, dans sa forme la plus simple de mon application se compose de trois ensembles:

  • L'application principale de l'assemblée qui consomment des plugins
  • Un assembly interop qui définit les types communs partagés par l'application et de ses plugins
  • Un exemple de plug-in de l'assemblée

Le code ci-dessous est une version simplifiée de mon vrai code, ne montrant que ce qui est nécessaire pour découvrir et charger des plugins, chacun dans sa propre AppDomain:

En commençant par l'application principale de l'assemblée, le programme principal de la classe utilise une classe utilitaire nommé PluginFinder découvrir qualifiying plugin types dans toutes les assemblées désigné dans un dossier du plugin. Pour chacun de ces types, il crée alors une instance d'un sandox AppDomain (avec les autorisations de la zone internet) et l'utilise pour créer une instance de la découverte du type de plugin.

Lors de la création d'un AppDomain avec des autorisations limitées, il est possible de spécifier un ou plusieurs de confiance des assemblées qui ne sont pas soumis à ces autorisations. Pour ce faire dans le scénario présenté ici, l'application principale de l'assemblée et de ses dépendances (l'assembly interop) doit être signé.

Pour chaque plugin chargé exemple, les méthodes personnalisées dans le plugin peut être appelé par l'intermédiaire de son interface connue et le plugin peut aussi appel à l'application ordinateur hôte par l'intermédiaire de son interface connue. Enfin, l'application hôte de décharge de chaque de la sandbox de domaines.

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

Dans cet exemple de code, l'application hôte classe est très simple, à exposer une méthode qui peut être appelé par les plugins. Cependant, cette classe doit découler MarshalByRefObject , de sorte qu'elle peut être référencée entre les domaines d'application.

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

L' PluginFinder classe a seulement une méthode publique qui renvoie une liste de découvert plugin types. Ce processus de découverte des charges de chaque assemblée qu'il trouve et utilise la réflexion pour identifier sa qualification types. Comme ce processus peut potentiellement charge de nombreuses assemblées (dont certains ne sont même pas contenir plugin types), il est également exécutée dans un autre domaine d'application, qui peut être subsequntly déchargé. Notez que cette classe hérite également MarshalByRefObject , pour les raisons décrites ci-dessus. Depuis que les instances de l' Type peut ne pas être passé entre les domaines d'application, ce processus de découverte utilise un type personnalisé appelé TypeLocator pour stocker le nom de la chaîne et le nom de l'assembly de chaque type découvert, qui peut ensuite être passé en toute sécurité de retour à la principale de l'application iphone de domaine.

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

L'interop assembly contient la classe de base pour les classes qui implémentent plugin fonctionnalité (notez qu'il résulte également de l' MarshalByRefObject.

Cette assemblée a également définit l' IHost interface qui permet aux plugins de retour d'appel dans l'application hôte.

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

Enfin, chaque plugin dérive de la classe de base définis dans l'assembly interop et met en œuvre ses méthodes abstraites. Il peut y avoir plusieurs classes héritant dans n'importe quel plugin de l'assemblée et il peut y avoir plusieurs plugin assemblées.

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

12voto

Alastair Maw Points 1549

Parce que vous êtes dans différents domaines d'application, vous ne pouvez pas vous passer de l'instance à travers.

Vous aurez besoin pour faire de vos plugins Distantes, et de créer un proxy dans votre application principale. Jetez un oeil à la doc de CreateInstanceAndUnWrap, qui est un exemple de la façon dont cela pourrait fonctionner vers le bas.

C'est aussi une autre beaucoup plus large aperçu par Jon Shemitz qui je pense est une bonne lecture. Bonne chance.

4voto

dthorpe Points 23314

Si vous avez besoin de votre 3ème partie extensions à la charge avec une baisse des privilèges de sécurité que le reste de votre application, vous devez créer un nouveau domaine d'application, créez un conteneur MEF pour vos extensions dans le domaine de l'application, puis marshall appels à partir de votre application pour les objets dans le bac à sable domaine de l'application. Le "bac à sable" se produit dans la façon dont vous créez le domaine de l'application, il n'a rien à avec le MEF.

1voto

Panos Rontogiannis Points 2560

Merci pour partager avec nous la solution. Je voudrais faire une observation importante et une sugestion.

Le commentaire est que vous ne pouvez pas 100% sandbox un plugin de le charger dans un autre domaine d'application à partir de l'hôte. Pour le savoir, mise à jour DoSomethingDangerous à la suivante:

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

Une exception non gérée soulevées par un enfant de fil peuvent provoquer une panne de l'ensemble de l'application.

Lire ceci pour plus d'informations concernant unhandle exceptions.

Vous pouvez aussi lire ces deux entrées de blog à partir du Système.AddIn équipe qui expliquent que 100% de l'isolement ne peut être lorsque le complément est dans un processus différent. Ils ont aussi un exemple de ce que quelqu'un peut le faire pour obtenir des notifications de compléments qui ne parviennent pas à gérer ont soulevé des exceptions.

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

Maintenant, la sugestion que je voulais faire de a à faire avec la PluginFinder.FindPlugins méthode. Au lieu de charger chaque candidat à l'assemblée dans un nouveau domaine d'application, en y réfléchissant de types et de déchargement, le domaine d'application, vous pouvez l'utiliser en Mono.Cecil. Ensuite, vous n'aurez pas à faire tout cela.

C'est aussi simple que:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

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

Il y a probablement de meilleures façons de faire cela avec Cecil, mais je ne suis pas un expert de l'utilisateur de cette bibliothèque.

En ce qui concerne,

0voto

zespri Points 6865

Une alternative serait d'utiliser cette bibliothèque: https://processdomain.codeplex.com/ Il permet l'exécution de tout .NET code en out-of-process domaine d'application, qui offre encore plus de l'isolement, de la accepté de répondre. Bien sûr, on doit choisir un bon outil pour la tâche qui leur incombe et dans de nombreux cas, l'approche adoptée dans la accepté de répondre est tout ce qui est nécessaire.

Toutefois, si vous travaillez avec .net plugins appel dans les bibliothèques natives qui peuvent être instables (la situation personnellement, je suis tombé sur) que vous souhaitez exécuter, non seulement dans un autre domaine d'application, mais aussi dans un processus séparé. Une caractéristique intéressante de cette bibliothèque est qu'il redémarre automatiquement le processus si un plugin plante.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X