60

.NET アプリケーションからプラグインにアクセスするためのシンプルで安全な方法を探しています。これは非常に一般的な要件だと思いますが、すべてのニーズを満たすものを見つけるのに苦労しています。

  • ホスト アプリケーションは、実行時にプラグイン アセンブリを検出してロードします。
  • プラグインは未知のサードパーティによって作成されるため、悪意のあるコードの実行を防ぐためにサンドボックス化する必要があります
  • 共通の相互運用アセンブリには、ホストとそのプラグインの両方によって参照される型が含まれます
  • 各プラグイン アセンブリには、共通のプラグイン インターフェイスを実装する 1 つ以上のクラスが含まれます。
  • プラグイン インスタンスを初期化するとき、ホストはそれ自体への参照をホスト インターフェイスの形式で渡します。
  • ホストは共通インターフェースを介してプラグインを呼び出し、プラグインは同様にホストを呼び出すことができます
  • ホストとプラグインは、相互運用アセンブリで定義された型 (ジェネリック型を含む) の形式でデータを交換します。

私は MEF と MAF の両方を調査しましたが、いずれかを法案に適合させる方法を見つけるのに苦労しています。

私の理解が正しいと仮定すると、MAF は分離境界を越えてジェネリック型を渡すことをサポートできません。これは私のアプリケーションにとって不可欠です。(MAFも実装が非常に複雑ですが、ジェネリック型の問題を解決できれば、これを使用する準備ができています)。

MEF はほぼ完璧なソリューションですが、拡張アセンブリをホストと同じ AppDomain にロードするため、セキュリティ要件を満たしていないように見えます。

MEF をサンドボックス モードで実行する方法について説明しているこの質問を見たことがありますが、その方法については説明していません。この投稿には、「MEF を使用する場合は、拡張機能が悪意のあるコードを実行しないことを信頼するか、コード アクセス セキュリティによる保護を提供する必要がある」と記載されていますが、その方法については説明されていません。最後に、未知のプラグインが読み込まれないようにする方法を説明しているこの投稿がありますが、正当なプラグインでさえ未知であるため、これは私の状況には適していません。

アセンブリに .NET 4.0 セキュリティ属性を適用することに成功し、それらは MEF によって正しく尊重されますが、これが悪意のあるコードをロックアウトするのにどのように役立つかわかりません。のメソッドなどSystem.IO.File) は としてマークされています。これは、アセンブリSecuritySafeCriticalからアクセスできることを意味します。SecurityTransparentここで何か不足していますか?プラグイン アセンブリにインターネット権限を提供する必要があることを MEF に伝えるために実行できる追加の手順はありますか?

最後に、ここで説明されているように、個別の AppDomain を使用して、独自の単純なサンドボックス プラグイン アーキテクチャを作成することも検討しました。しかし、私の知る限り、この手法では遅延バインディングを使用して、信頼されていないアセンブリ内のクラスで静的メソッドを呼び出すことしかできません。このアプローチを拡張してプラグイン クラスの 1 つのインスタンスを作成しようとすると、返されたインスタンスを共通のプラグイン インターフェイスにキャストできません。つまり、ホスト アプリケーションがそれを呼び出すことができません。AppDomain 境界を越えて厳密に型指定されたプロキシ アクセスを取得するために使用できる手法はありますか?

この質問が長くなってしまい申し訳ありません。その理由は、誰かが何か新しい試みを提案できることを期待して、私がすでに調査したすべての道を示すことでした.

あなたのアイデアに感謝します、ティム

4

5 に答える 5

55

Alastair Maw の回答を受け入れました。彼の提案とリンクが実行可能な解決策につながったのですが、同様のことを達成しようとしている可能性のある他の人のために、私が行ったことの詳細をここに投稿しています。

私のアプリケーションの最も単純な形式は、次の 3 つのアセンブリで構成されることを思い出してください。

  • プラグインを使用するメイン アプリケーション アセンブリ
  • アプリケーションとそのプラグインによって共有される共通の型を定義する相互運用アセンブリ
  • サンプル プラグイン アセンブリ

以下のコードは、実際のコードを簡略化したもので、プラグインの検出と読み込みに必要なものだけを、それぞれ独自の .xml で示していますAppDomain

メイン アプリケーション アセンブリから始めて、メイン プログラム クラスは という名前のユーティリティ クラスを使用してPluginFinder、指定されたプラグイン フォルダー内の任意のアセンブリ内の適切なプラグイン タイプを検出します。次に、これらのタイプごとに、サンドックスのインスタンスをAppDomain(インターネット ゾーンのアクセス許可を使用して) 作成し、それを使用して、検出されたプラグイン タイプのインスタンスを作成します。

アクセス許可が制限された を作成する場合、AppDomainそれらのアクセス許可が適用されない 1 つ以上の信頼されたアセンブリを指定できます。ここに示すシナリオでこれを実現するには、メイン アプリケーション アセンブリとその依存関係 (相互運用アセンブリ) に署名する必要があります。

読み込まれたプラグイン インスタンスごとに、プラグイン内のカスタム メソッドを既知のインターフェイス経由で呼び出すことができ、プラグインは既知のインターフェイス経由でホスト アプリケーションにコールバックすることもできます。最後に、ホスト アプリケーションは各サンドボックス ドメインをアンロードします。

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

このサンプル コードでは、ホスト アプリケーション クラスは非常に単純で、プラグインによって呼び出されるメソッドを 1 つだけ公開しています。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クラスには、検出されたプラグイン タイプのリストを返す public メソッドが 1 つだけあります。この検出プロセスでは、検出された各アセンブリが読み込まれ、リフレクションを使用して該当する型が識別されます。このプロセスは多くのアセンブリをロードする可能性があるため (プラグイン タイプを含まないアセンブリもあります)、別のアプリケーション ドメインでも実行され、後でアンロードされる可能性があります。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();           
    }
}
于 2010-11-11T17:24:13.417 に答える
12

異なる AppDomains にいるため、インスタンスを単に渡すことはできません。

プラグインをリモート可能にし、メイン アプリでプロキシを作成する必要があります。CreateInstanceAndUnWrapのドキュメントを見てください。これには、これがすべて下に向かってどのように機能するかの例があります。

これは、Jon Shemitz による別のより広範な概観でもあり、私は良い読み物だと思います。幸運を。

于 2010-11-10T18:19:44.877 に答える
4

アプリの残りの部分よりも低いセキュリティ権限でサード パーティの拡張機能を読み込む必要がある場合は、新しい AppDomain を作成し、そのアプリ ドメインに拡張機能用の MEF コンテナーを作成してから、アプリケーションからオブジェクトへの呼び出しをマーシャリングする必要があります。サンドボックス化されたアプリ ドメインで。サンドボックス化はアプリ ドメインの作成方法で発生し、MEF とは関係ありません。

于 2010-11-10T18:23:49.513 に答える
1

解決策を共有していただきありがとうございます。重要なコメントと提案をしたいと思います。

コメントは、ホストとは異なる AppDomain にプラグインをロードして、プラグインを 100% サンドボックス化することはできないということです。確認するには、DoSomethingDangerous を次のように更新します。

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

子スレッドによって発生した未処理の例外は、アプリケーション全体をクラッシュさせる可能性があります。

アンハンドル例外に関する情報については、こちらをお読みください。

また、アドインが別のプロセスにある場合にのみ 100% の分離が可能であることを説明している System.AddIn チームの 2 つのブログ エントリを読むこともできます。また、発生した例外の処理に失敗したアドインから通知を受け取るために何を実行できるかの例も示しています。

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

ここで、私が作りたかった提案は、PluginFinder.FindPlugins メソッドに関係しています。各候補アセンブリを新しい AppDomain にロードし、その型を反映して AppDomain をアンロードする代わりに、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;
    }
}

おそらく、Cecil でこれを行うより良い方法がありますが、私はこのライブラリのエキスパート ユーザーではありません。

よろしく、

于 2012-04-04T09:16:33.243 に答える