방법: 샌드박스에서 부분 신뢰 코드 실행

샌드박싱은 코드에 대해 액세스 권한이 제한적으로 부여되어 있는 제한된 보안 환경에서 코드를 실행하는 절차입니다. 예를 들어 관리되는 라이브러리가 있지만 그 출처를 완전히 신뢰할 수는 없으면 해당 라이브러리를 완전히 신뢰할 수 있는 것으로 실행하지 말아야 합니다. 대신 Execution 권한 등 필요할 것으로 예상되는 권한만 제한적으로 부여된 샌드박스에 코드를 배치해야 합니다.

또한 샌드박싱을 사용하면 부분적으로 신뢰되는 환경에서 실행되는 코드를 배포하기 전에 미리 테스트할 수도 있습니다.

AppDomain을 사용하면 관리되는 응용 프로그램에 대한 샌드박스를 효과적으로 제공할 수 있습니다. 부분적으로 신뢰되는 코드를 실행하는 데 사용되는 응용 프로그램 도메인에는 이를 AppDomain 내에서 실행할 때 사용 가능한 보호되는 리소스를 정의하는 권한이 있습니다. AppDomain 내에서 실행되는 코드는 AppDomain과 관련된 권한으로 경계가 설정되며 지정 리소스에 대한 액세스만 허용됩니다. AppDomain에는 완전히 신뢰할 수 있는 것으로 로드해야 할 어셈블리를 식별하는 데 사용되는 StrongName 배열도 포함되어 있습니다. 이를 통해 AppDomain 작성자가 특정 도우미 어셈블리를 완전히 신뢰할 수 있도록 허용하는 새로운 샌드박스가 적용된 도메인을 시작할 수 있습니다. 어셈블리를 완전히 신뢰할 수 있는 것으로 로드하는 또 다른 방법은 해당 어셈블리를 전역 어셈블리 캐시에 배치하는 것입니다. 그러나 이 방법을 사용하면 해당 컴퓨터에 만든 모든 응용 프로그램 도메인에서 어셈블리를 완전히 신뢰할 수 있는 것으로 로드하게 됩니다. 강력한 이름 목록에서는 신뢰 여부를 AppDomain별로 결정할 수 있도록 지원하므로 더 제한적인 판단이 가능합니다.

AppDomain.CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) 메서드 오버로드를 사용하여 샌드박스에서 실행되는 응용 프로그램에 대한 권한 집합을 지정할 수 있습니다. 이 오버로드를 사용하면 원하는 코드 액세스 보안의 정확한 수준을 지정할 수 있습니다. 이 오버로드를 사용하여 AppDomain으로 로드한 어셈블리는 지정된 권한 집합만 갖거나 완전히 신뢰될 수 있습니다. 전역 어셈블리 캐시에 있거나 fullTrustAssemblies(StrongName) 배열 매개 변수에 나열된 어셈블리는 완전한 신뢰를 받습니다. 완전히 신뢰할 수 있는 것으로 알려진 어셈블리만 fullTrustAssemblies 목록에 추가해야 합니다.

오버로드에는 다음과 같은 시그니처가 있습니다.

AppDomain.CreateDomain( string friendlyName,
                        Evidence securityInfo,
                        AppDomainSetup info,
                        PermissionSet grantSet,
                        params StrongName[] fullTrustAssemblies);

CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) 메서드 오버로드의 매개 변수는 AppDomain의 이름, AppDomain에 대한 증명 정보, 샌드박스에 대한 응용 프로그램 기준 위치를 식별하는 AppDomainSetup 개체, 사용할 권한 집합 및 완전히 신뢰할 수 있는 어셈블리의 강력한 이름을 지정합니다.

보안상의 이유로 info 매개 변수에 지정된 응용 프로그램 기준은 호스팅 응용 프로그램에 대한 응용 프로그램 기준이면 안 됩니다.

grantSet 매개 변수의 경우 명시적으로 만든 권한 집합이나 GetStandardSandbox 메서드로 만들어진 표준 권한 집합 중 하나를 지정할 수 있습니다.

대부분의 AppDomain 로드와 달리 securityInfo 매개 변수에서 제공하는 AppDomain에 대한 증명 정보는 부분적으로 신뢰되는 어셈블리에 대한 권한 부여 설정을 결정하는 데 사용되는 대신 grantSet 매개 변수에서 독립적으로 지정됩니다. 그러나 증명 정보는 격리된 저장소 범위 확인과 같은 다른 용도로 사용될 수 있습니다.

샌드박스에서 응용 프로그램을 실행하려면

  1. 신뢰할 수 없는 응용 프로그램에 부여할 권한 집합을 만듭니다. 부여할 수 있는 최소 권한은 Execution 권한입니다. IsolatedStorageFilePermission 등과 같이 신뢰할 수 없는 코드에도 안전할 것으로 생각되는 권한을 추가로 부여할 수도 있습니다. 다음 코드에서는 Execution 권한만 포함된 새 권한 집합을 만듭니다.

    PermissionSet permSet = new PermissionSet(PermissionState.None);
    permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));
    

    또는 Internet 같은 기존의 명명된 권한 집합을 사용할 수 있습니다.

    Evidence ev = new Evidence();
    ev.AddHostEvidence(new Zone(SecurityZone.Internet));
    PermissionSet internetPS = SecurityManager.GetStandardSandbox(ev);
    

    GetStandardSandbox 메서드는 증명 정보의 영역에 따라 Internet 권한 집합 또는 LocalIntranet 권한 집합을 반환합니다. GetStandardSandbox에서는 참조로 전달된 증명 정보 개체 중 일부에 대한 ID 권한도 만듭니다.

  2. 신뢰할 수 없는 코드를 호출하는 호스팅 클래스(이 예제의 경우 Sandboxer)가 포함된 어셈블리에 서명합니다. 어셈블리에 서명하는 데 사용된 StrongNameCreateDomain 호출의 fullTrustAssemblies 매개 변수가 포함된 StrongName 배열에 추가합니다. 부분 신뢰 코드를 실행하거나 부분 신뢰 응용 프로그램에 서비스를 제공하기 위해서는 호스팅 클래스를 완전히 신뢰할 수 있는 것으로 실행해야 합니다. 어셈블리의 StrongName은 다음과 같이 읽을 수 있습니다.

    StrongName fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<StrongName>();
    

    mscorlib 및 System.dll 등과 같은 .NET Framework 어셈블리는 전역 어셈블리 캐시에서 완전히 신뢰할 수 있는 것으로 로드되므로 완전 신뢰 목록에 추가할 필요가 없습니다.

  3. CreateDomain 메서드의 AppDomainSetup 매개 변수를 초기화합니다. 이 매개 변수를 사용하면 새 AppDomain의 여러 가지 설정을 제어할 수 있습니다. ApplicationBase 속성은 중요한 설정이며 호스팅 응용 프로그램의 AppDomain에 대한 ApplicationBase 속성과 달라야 합니다. ApplicationBase 설정이 동일하면 부분 신뢰 응용 프로그램에서 해당 응용 프로그램을 통해 정의되는 예외를 호스팅 응용 프로그램이 완전히 신뢰할 수 있는 것으로 로드하게 하여 이를 사용할 수 있습니다. catch(예외)를 사용하지 않는 것이 좋은 이유가 여기에도 있습니다. 호스트의 응용 프로그램 기준 위치를 샌드박싱된 응용 프로그램의 응용 프로그램 기준 위치와 다르게 설정하면 이와 같은 악용의 위험을 줄일 수 있습니다.

    AppDomainSetup adSetup = new AppDomainSetup();
    adSetup.ApplicationBase = Path.GetFullPath(pathToUntrusted);
    
  4. CreateDomain(String, Evidence, AppDomainSetup, PermissionSet, StrongName[]) 메서드 오버로드를 호출하여 앞서 지정한 매개 변수로 응용 프로그램 도메인을 만듭니다.

    이 메서드의 시그니처는 다음과 같습니다.

    public static AppDomain CreateDomain(string friendlyName, 
        Evidence securityInfo, AppDomainSetup info, PermissionSet grantSet, 
        params StrongName[] fullTrustAssemblies)
    

    추가 정보:

    • 이는 PermissionSet을 매개 변수로 취하는 CreateDomain 메서드의 유일한 오버로드이므로 부분 신뢰 설정으로 응용 프로그램을 로드하는 데 사용할 수 있는 유일한 오버로드이기도 합니다.

    • evidence 매개 변수는 권한 집합을 계산하는 데 사용되지 않습니다. 이 매개 변수는 .NET Framework의 다른 기능을 통한 식별에 사용됩니다.

    • 이 오버로드에 대해서는 info 매개 변수의 ApplicationBase 속성을 반드시 설정해야 합니다.

    • fullTrustAssemblies 매개 변수에는 params 키워드가 있습니다. 즉, StrongName 배열을 반드시 만들지 않아도 됩니다. 강력한 이름을 매개 변수로 전달하지 않거나 한 개 이상의 강력한 이름을 매개 변수로 전달할 수 있습니다.

    • 응용 프로그램 도메인을 만드는 코드는 다음과 같습니다.

    AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);
    
  5. 앞서 만든 샌드박싱 AppDomain으로 코드를 로드합니다. 이 작업은 다음 두 가지 방법으로 수행할 수 있습니다.

    가능하면 둘째 방법을 사용하는 것이 더 좋습니다. 새 AppDomain 인스턴스에 매개 변수를 더 쉽게 전달할 수 있기 때문입니다. CreateInstanceFrom 메서드는 두 가지 중요한 기능을 제공합니다.

    • 어셈블리가 포함되지 않은 위치를 가리키는 코드베이스를 사용할 수 있습니다.

    • 완전 신뢰(PermissionState.Unrestricted)에 대한 Assert를 호출한 후 인스턴스를 만들 수 있으므로 중요 클래스의 인스턴스를 만드는 것이 가능합니다. 어셈블리에 투명 표시가 되어 있지 않고 어셈블리를 완전히 신뢰할 수 있는 것으로 로드하는 경우 이러한 상황이 발생합니다. 따라서 이 기능을 사용할 때는 신뢰할 수 있는 코드만 만들도록 주의를 기울여야 합니다. 새 응용 프로그램 도메인에는 완전히 신뢰할 수 있는 클래스의 인스턴스만 만드는 것이 좋습니다.

    ObjectHandle handle = Activator.CreateInstanceFrom(
    newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,
           typeof(Sandboxer).FullName );
    

    새 도메인에 클래스의 인스턴스를 만들려면 해당 클래스에서 MarshalByRefObject 클래스를 확장해야 합니다.

    class Sandboxer:MarshalByRefObject
    
  6. 새 도메인 인스턴스를 이 도메인의 참조로 래핑 해제합니다. 이 참조는 신뢰할 수 없는 코드를 실행하는 데 사용됩니다.

    Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();
    
  7. 방금 만든 Sandboxer 클래스의 인스턴스에서 ExecuteUntrustedCode 메서드를 호출합니다.

    newDomainInstance.ExecuteUntrustedCode(untrustedAssembly, untrustedClass, entryPoint, parameters);
    

    이 호출은 샌드박싱된 응용 프로그램 도메인에서 실행되므로 권한이 제한됩니다.

    public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)
        {
            //Load the MethodInfo for a method in the new assembly. This might be a method you know, or 
            //you can use Assembly.EntryPoint to get to the entry point in an executable.
            MethodInfo target = Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
            try
            {
                // Invoke the method.
                target.Invoke(null, parameters);
            }
            catch (Exception ex)
            {
            //When information is obtained from a SecurityException extra information is provided if it is 
            //accessed in full-trust.
                (new PermissionSet(PermissionState.Unrestricted)).Assert();
                Console.WriteLine("SecurityException caught:\n{0}", ex.ToString());
    CodeAccessPermission.RevertAssert();
                Console.ReadLine();
            }
        }
    

    System.Reflection은 부분적으로 신뢰할 수 있는 어셈블리의 메서드 핸들을 얻는 데 사용됩니다. 이 핸들을 사용하여 코드에 최소한의 권한만 부여한 채 안전하게 코드를 실행할 수 있습니다.

    이전 코드에서는 SecurityException을 출력하기 전에 완전 신뢰 권한에 대한 Assert가 호출되었음을 유의하십시오.

    new PermissionSet(PermissionState.Unrestricted)).Assert()
    

    완전 신뢰 어설션은 SecurityException에서 확장된 정보를 가져오는 데 사용됩니다. Assert가 없으면 SecurityExceptionToString 메서드에서 스택에 부분적으로 신뢰할 수 있는 코드가 있다는 사실을 발견하고 반환되는 정보를 제한합니다. 이 경우 부분 신뢰 코드에서 해당 정보를 읽을 수 있으면 보안 문제가 발생할 수 있습니다. 그러나 UIPermission을 부여하지 않으면 이러한 위험을 줄일 수 있습니다. 완전 신뢰 어설션은 반드시 필요한 경우에 한해, 부분 신뢰 코드의 권한이 완전 신뢰 수준으로 상승되지 않는다는 것이 확실할 때만 사용해야 합니다. 일반적으로 완전 신뢰에 대한 어설션을 호출한 후에는 동일한 함수에서 신뢰할 수 없는 코드를 호출하지 않아야 합니다. 어설션의 사용을 마쳤으면 항상 이를 되돌리는 습관을 갖는 것이 좋습니다.

예제

다음 예제에서는 위 단원에서 설명한 절차를 실제로 구현합니다. 이 예제에서 Visual Studio 솔루션의 Sandboxer라는 프로젝트에는 UntrustedClass 클래스를 구현하는 UntrustedCode라는 프로젝트도 포함됩니다. 이 시나리오에서는 사용자가 제공한 숫자가 피보나치 수인지 여부를 나타내는 true 또는 false를 반환한다고 알려진 메서드가 포함된 라이브러리 어셈블리를 다운로드한 것으로 가정합니다. 그러나 알려진 바와 달리 메서드는 컴퓨터에서 파일을 읽으려고 시도합니다. 다음 예제에서는 신뢰할 수 없는 코드를 보여 줍니다.

using System;
using System.IO;
namespace UntrustedCode
{
    public class UntrustedClass
    {
        // Pretend to be a method checking if a number is a Fibonacci
        // but which actually attempts to read a file.
        public static bool IsFibonacci(int number)
        {
           File.ReadAllText("C:\\Temp\\file.txt");
           return false;
        }
    }
}

다음 예제에서는 신뢰할 수 없는 코드를 실행하는 Sandboxer 응용 프로그램 코드를 보여 줍니다.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Security;
using System.Security.Policy;
using System.Security.Permissions;
using System.Reflection;
using System.Runtime.Remoting;

//The Sandboxer class needs to derive from MarshalByRefObject so that we can create it in another 
// AppDomain and refer to it from the default AppDomain.
class Sandboxer : MarshalByRefObject
{
    const string pathToUntrusted = @"..\..\..\UntrustedCode\bin\Debug";
    const string untrustedAssembly = "UntrustedCode";
    const string untrustedClass = "UntrustedCode.UntrustedClass";
    const string entryPoint = "IsFibonacci";
    private static Object[] parameters = { 45 };
    static void Main()
    {
        //Setting the AppDomainSetup. It is very important to set the ApplicationBase to a folder 
        //other than the one in which the sandboxer resides.
        AppDomainSetup adSetup = new AppDomainSetup();
        adSetup.ApplicationBase = Path.GetFullPath(pathToUntrusted);

        //Setting the permissions for the AppDomain. We give the permission to execute and to 
        //read/discover the location where the untrusted code is loaded.
        PermissionSet permSet = new PermissionSet(PermissionState.None);
        permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));

        //We want the sandboxer assembly's strong name, so that we can add it to the full trust list.
        StrongName fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<StrongName>();

        //Now we have everything we need to create the AppDomain, so let's create it.
        AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);

        //Use CreateInstanceFrom to load an instance of the Sandboxer class into the
        //new AppDomain. 
        ObjectHandle handle = Activator.CreateInstanceFrom(
            newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,
            typeof(Sandboxer).FullName
            );
        //Unwrap the new domain instance into a reference in this domain and use it to execute the 
        //untrusted code.
        Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();
        newDomainInstance.ExecuteUntrustedCode(untrustedAssembly, untrustedClass, entryPoint, parameters);
    }
    public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)
    {
        //Load the MethodInfo for a method in the new Assembly. This might be a method you know, or 
        //you can use Assembly.EntryPoint to get to the main function in an executable.
        MethodInfo target = Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
        try
        {
            //Now invoke the method.
            bool retVal = (bool)target.Invoke(null, parameters);
        }
        catch (Exception ex)
        {
            // When we print informations from a SecurityException extra information can be printed if we are 
            //calling it with a full-trust stack.
            (new PermissionSet(PermissionState.Unrestricted)).Assert();
            Console.WriteLine("SecurityException caught:\n{0}", ex.ToString());
            CodeAccessPermission.RevertAssert();
            Console.ReadLine();
        }
    }
}

참고 항목

개념

보안 코딩 지침