developer tip

샌드 박싱 .NET 플러그인에 대한 실용적인 접근 방식을 찾고 있습니다.

optionbox 2020. 12. 9. 08:04
반응형

샌드 박싱 .NET 플러그인에 대한 실용적인 접근 방식을 찾고 있습니다.


.NET 응용 프로그램에서 플러그인에 액세스하는 간단하고 안전한 방법을 찾고 있습니다. 이것이 매우 일반적인 요구 사항이라고 생각하지만 모든 요구 사항을 충족하는 것을 찾기 위해 고군분투하고 있습니다.

  • 호스트 애플리케이션은 런타임에 플러그인 어셈블리를 검색하고로드합니다.
  • 플러그인은 알려지지 않은 제 3 자에 의해 생성되므로 악성 코드 실행을 방지하기 위해 샌드 박스 처리해야합니다.
  • 공통 interop 어셈블리에는 호스트와 해당 플러그인 모두에서 참조하는 유형이 포함됩니다.
  • 각 플러그인 어셈블리에는 공통 플러그인 인터페이스를 구현하는 하나 이상의 클래스가 포함됩니다.
  • 플러그인 인스턴스를 초기화 할 때 호스트는 호스트 인터페이스 형식으로 자신에 대한 참조를 전달합니다.
  • 호스트는 공통 인터페이스를 통해 플러그인을 호출하고 플러그인은 마찬가지로 호스트를 호출 할 수 있습니다.
  • 호스트와 플러그인은 interop 어셈블리에 정의 된 형식 (일반 형식 포함)의 형식으로 데이터를 교환합니다.

MEF와 MAF를 모두 조사했지만 둘 중 하나가 청구서에 맞게 어떻게 만들어 질 수 있는지 확인하기 위해 고군분투하고 있습니다.

내 이해가 정확하다고 가정하면 MAF는 내 응용 프로그램에 필수적인 격리 경계를 넘어 제네릭 형식을 전달하는 것을 지원할 수 없습니다. (MAF는 구현하기가 매우 복잡하지만 제네릭 유형 문제를 해결할 수 있다면이 작업을 준비 할 수 있습니다.)

MEF는 거의 완벽한 솔루션이지만 호스트와 동일한 AppDomain에 확장 어셈블리를로드하여 샌드 박싱을 방지하기 때문에 보안 요구 사항에 미치지 못하는 것으로 보입니다.

샌드 박스 모드에서 MEF를 실행하는 것에 대해 이야기하는 이 질문 을 보았지만 방법은 설명하지 않습니다. 이 게시물 은 "MEF를 사용할 때 악성 코드를 실행하거나 코드 액세스 보안을 통해 보호 기능을 제공하지 않도록 확장 프로그램을 신뢰해야합니다."라고 언급하지만, 방법은 설명하지 않습니다. 마지막으로 알 수없는 플러그인이로드되는 것을 방지하는 방법을 설명하는 이 게시물이 있지만 합법적 인 플러그인도 알 수 없기 때문에 제 상황에는 적합하지 않습니다.

.NET 4.0 보안 특성을 어셈블리에 적용하는 데 성공했으며 MEF에서 올바르게 존중하지만 보안 위협이 될 수있는 많은 프레임 워크 메서드처럼 이것이 악성 코드를 잠그는 데 어떻게 도움이되는지 모르겠습니다 ( 의 메서드와 같은 System.IO.File)은로 표시되어 어셈블리 SecuritySafeCritical에서 액세스 할 수 있음을 의미합니다 SecurityTransparent. 여기에 뭔가 빠졌나요? 플러그인 어셈블리에 인터넷 권한을 제공해야한다고 MEF에 알리기 위해 취할 수있는 추가 단계가 있습니까?

마지막으로 여기에 설명 된대로 별도의 AppDomain을 사용하여 간단한 샌드 박스 플러그인 아키텍처를 만드는 방법도 살펴 보았습니다 . 그러나 내가 볼 수있는 한이 기술을 사용하면 후기 바인딩을 사용하여 신뢰할 수없는 어셈블리의 클래스에서 정적 메서드를 호출 할 수 있습니다. 이 접근 방식을 확장하여 플러그인 클래스 중 하나의 인스턴스를 만들려고하면 반환 된 인스턴스를 공통 플러그인 인터페이스로 캐스팅 할 수 없습니다. 즉, 호스트 응용 프로그램이이 인터페이스를 호출 할 수 없습니다. AppDomain 경계에서 강력한 형식의 프록시 액세스를 얻는 데 사용할 수있는 몇 가지 기술이 있습니까?

이 질문의 길이에 대해 사과드립니다. 그 이유는 누군가가 새로운 시도를 제안 할 수 있기를 바라면서 제가 이미 조사한 모든 길을 보여주기 위해서였습니다.

여러분의 아이디어에 감사드립니다, Tim


다른 AppDomain에 있기 때문에 인스턴스를 전달할 수 없습니다.

플러그인을 원격 가능하게 만들고 기본 앱에서 프록시를 만들어야합니다. 모든 것이 어떻게 바닥을 향해 작동 할 수 있는지에 대한 예제가있는 CreateInstanceAndUnWrap 문서를 살펴보십시오 .

이것은 또한 Jon Shemitz의 또 다른 훨씬 더 광범위한 개요 이며 좋은 읽기라고 생각합니다. 행운을 빕니다.


나는 Alastair Maw의 답변을 받아 들였습니다. 그의 제안과 링크가 나를 실행 가능한 솔루션으로 이끄는 것이었지만, 비슷한 것을 달성하려는 다른 사람들을 위해 내가 한 일에 대한 세부 정보를 여기에 게시하고 있습니다.

참고로, 가장 간단한 형태의 애플리케이션은 세 개의 어셈블리로 구성됩니다.

  • 플러그인을 사용할 기본 애플리케이션 어셈블리
  • 애플리케이션 및 해당 플러그인에서 공유하는 공통 유형을 정의하는 interop 어셈블리
  • 샘플 플러그인 어셈블리

아래 코드는 내 실제 코드의 단순화 된 버전으로, 각각 자체적으로 플러그인을 검색하고로드하는 데 필요한 사항 만 보여줍니다 AppDomain.

기본 응용 프로그램 어셈블리부터 기본 프로그램 클래스는 PluginFinder지정된 플러그인 폴더의 모든 어셈블리 내에서 자격을 갖춘 플러그인 유형을 검색하기 위해 명명 된 유틸리티 클래스를 사용 합니다. 그런 다음 각 유형에 대해 sandox 인스턴스 AppDomain(인터넷 영역 권한 포함)를 생성하고이를 사용하여 검색된 플러그인 유형의 인스턴스를 생성합니다.

AppDomain제한된 권한으로를 만들 때 해당 권한이 적용되지 않는 하나 이상의 신뢰할 수있는 어셈블리를 지정할 수 있습니다. 여기에 제시된 시나리오에서이를 수행하려면 기본 애플리케이션 어셈블리 및 해당 종속성 (interop 어셈블리)에 서명해야합니다.

로드 된 각 플러그인 인스턴스에 대해 플러그인 내의 사용자 지정 메서드는 알려진 인터페이스를 통해 호출 될 수 있으며 플러그인은 알려진 인터페이스를 통해 호스트 응용 프로그램을 다시 호출 할 수도 있습니다. 마지막으로 호스트 응용 프로그램은 각 샌드 박스 도메인을 언로드합니다.

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

The interop assembly contains the base class for classes that will implement plugin functionality (note that it also derives from MarshalByRefObject.

This assembly also defines the IHost interface that enables plugins to call back into the host application.

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

Finally, each plugin derives from the base class defined in the interop assembly and implements its abstract methods. There may be multiple inheriting classes in any plugin assembly and there may be multiple plugin assemblies.

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

If you need your 3rd party extensions to load with a lower security privileges than the rest of your app, you should create a new AppDomain, create a MEF container for your extensions in that app domain, and then marshall calls from your application to the objects in the sandboxed app domain. The sandboxing occurs in how you create the app domain, it has nothing to to with MEF.


Thanks for sharing with us the solution. I would like to make an important comment and a sugestion.

The comment is that you cannot 100% sandbox a plugin by loading it in a different AppDomain from the host. To find out, update DoSomethingDangerous to the following:

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

An unhandled exception raised by a child thread can crash the whole application.

Read this for information concerning unhandle exceptions.

You can also read these two blog entries from the System.AddIn team that explain that 100% isolation can only be when the add-in is in a different process. They also have an example of what someone can do to get notifications from add-ins that fail to handle raised 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

Now the sugestion that I wanted to make has to do with the PluginFinder.FindPlugins method. Instead of loading each candidate assembly in a new AppDomain, reflecting on it's types and the unload the AppDomain, you could use Mono.Cecil. You then will not have to do any of this.

It is as simple as:

AssemblyDefinition ad = AssemblyDefinition.ReadAssembly(assemblyPath);

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

There are probably even better ways to do this with Cecil but I am not an expert user of this library.

Regards,


An alternative would be to use this library: https://processdomain.codeplex.com/ It allows you running any .NET code in out-of-process AppDomain, which provides even better isolation, than the accepted answer. Of course one needs to choose a right tool for their task and in many cases the approach given in the accepted answer is all that is needed.

However if your are working with .net plugins that call into native libraries that may be unstable (the situation I personally came across) you want to run them not only in a separate app domain, but also in a separate process. A nice feature of this library is that it will automatically restarts the process if a plugin crashes it.

참고URL : https://stackoverflow.com/questions/4145713/looking-for-a-practical-approach-to-sandboxing-net-plugins

반응형