HOW TO:在沙箱中執行部分信任的程式碼

沙箱作業是指在限制的安全性環境中執行程式碼,這樣可限制授與程式碼的存取權限。 例如,如果您的來源中有您無法完全信任的 Managed 程式庫,您就不應以完全信任的形式執行。 此時,您應將程式碼置於沙箱中,將其權限限定於預期該程式碼需要的權限 (例如 Execution 權限)。

您也可以使用沙箱作業測試將要散發並且在部分信任環境中執行的程式碼。

AppDomain 是為 Managed 應用程式提供沙箱的一種有效方式。 用於執行部分信任程式碼的應用程式定義域具有的權限,會定義在該 AppDomain 內執行時可使用的受保護資源。 執行於 AppDomain 內的程式碼會受制於與 AppDomain 相關聯的權限,並且只能存取指定的資源。 AppDomain 也包含 StrongName 陣列,用以識別要以完全信任的形式載入的組件。 這樣可讓 AppDomain 的建立者啟動新的沙箱定義域,使特定 Helper 組件成為完全信任。 另一種以完全信任的形式載入組件的方式,是將組件置於全域組件快取中;但這將會在所有建立於電腦上的應用程式定義域中,以完全信任的形式載入組件。 強式名稱清單可支援以個別 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 載入,AppDomain 的辨識項 (由 securityInfo 參數提供) 並不是用來決定部分信任組件的授權集。 授權集是另外由 grantSet 參數指定的。 不過,辨識項可使用於其他用途,例如判斷隔離儲存區範圍。

若要在沙箱中執行應用程式

  1. 建立要對未受信任的應用程式授與的權限集合。 您可以授與的最低權限為 Execution 權限。 您也可以對未受信任的程式碼授與其他您認為應屬安全的權限,例如 IsolatedStorageFilePermission。 下列程式碼會建立僅具有 Execution 權限的新權限集合。

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

    或者,您也可以使用現有的具名權限集合,例如網際網路。

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

    GetStandardSandbox 方法會傳回 Internet 權限集合或 LocalIntranet 權限集合,視辨識項中的區域而定。 GetStandardSandbox 也會為某些傳入做為參考的辨識項物件建構識別權限。

  2. 簽署組件,該組件包含會呼叫未受信任之程式碼的裝載類別 (在此範例中名為 Sandboxer)。 將用以簽署組件的 StrongName 加入至 CreateDomain 呼叫之 fullTrustAssemblies 參數的 StrongName 陣列。 必須以完全信任的形式執行裝載類別,才能執行部分信任的程式碼,或為部分信任的應用程式提供服務。 組件的 StrongName 顯示如下:

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

    .NET Framework 組件 (如 mscorlib 與 System.dll) 無須加入至完全信任清單中,因為它們是以完全信任的形式從全域組件快取載入的。

  3. 初始化 CreateDomain 方法的 AppDomainSetup 參數。 使用此參數可讓您控制新 AppDomain 的許多設定。 ApplicationBase 屬性是一項重要的設定,且應不同於裝載應用程式之 AppDomainApplicationBase 屬性。 如果 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)
    

    其他資訊:

    • CreateDomain 方法只有此多載可採用 PermissionSet 做為參數,因此也只有此多載可讓您以部分信任的設定載入應用程式。

    • evidence 參數不會用來計算權限集合,而是供 .NET Framework 的其他功能進行識別之用。

    • 在使用此多載時,必須要設定 info 參數的 ApplicationBase 屬性。

    • fullTrustAssemblies 參數具有 params 關鍵字,這表示無須建立 StrongName 陣列。 傳入 0 個、1 個或更多強式名稱做為參數,都是可允許的。

    • 用以建立應用程式定義域的程式碼為:

    AppDomain newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);
    
  5. 將程式碼載入您所建立的沙箱 AppDomain 中。 有兩種方法可以達到這個目的:

    第二個方法較多人使用,因為此方法較容易將參數傳遞至新的 AppDomain 執行個體。 CreateInstanceFrom 方法提供了兩項重要功能:

    • 您可以使用其指向之位置不含您的組件的程式碼基底。

    • 您可以在完全信任的 Assert 下建立 (PermissionState.Unrestricted),這可讓您建立重要類別的執行個體。 (只要您的組件不含透明度標示,並且以完全信任的形式載入,即可執行上述作業。)因此,您在使用此功能建立程式碼時,必須謹慎地將其限定於您所信任的程式碼,此外建議您在新的應用程式定義域中僅建立完全信任之類別的執行個體。

    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 的擴充資訊。 若缺少 AssertSecurityExceptionToString 方法將會發現堆疊上有部分信任的程式碼,而會限制傳回的資訊。 如果部分信任程式碼能夠讀取該資訊,這就可能造成安全性問題,但只要不授與 UIPermission,即可消除此風險。 完全信任的判斷提示不應經常使用,並且只應在您確定不允許部分信任的程式碼提高為完全信任時使用。 因此,不要在相同的函式中以及在您呼叫完全信任的判斷提示後,呼叫您不信任的程式碼。 最好是在使用完判斷提示之後,一律將它還原。

範例

下列範例將實作上一節中的程序。 在此範例中,Visual Studio 解決方案中名為 Sandboxer 的專案同時也包含名為 UntrustedCode 的專案,而此專案會實作類別 UntrustedClass。 此情節假設您已下載程式庫組件,且其中所含的方法預定應傳回 true 或 false,以指出您所提供的數字是否為 Fibonacci 數字。 但此方法嘗試從您的電腦讀取檔案。 下列範例列出未受信任的程式碼。

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

請參閱

概念

安全程式碼撰寫方針