如何:运行沙盒中部分受信任的代码

沙盒是一种在受限的安全环境中运行代码的实际操作,这种操作限制授予的代码访问权限。 例如,如果您具有一个来自不完全信任源的托管库,则不应以完全受信任的方式运行它。 而是应将该代码放在沙盒中,以便将其权限限制到您预计它需要的范围内(例如,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 加载不同,不使用 AppDomain(由 securityInfo 参数提供)的证据来确定部分受信任程序集的授予集。 而是单独由 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 还能构造用于作为引用而传递的一些证据对象的标识权限。

  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 属性是一个重要设置,其应与 ApplicationBase 属性(用于该承载应用程序的 AppDomain)不同。 如果 ApplicationBase 设置是相同的,则部分信任应用程序可以获取要以完全受信任方式加载它所定义的异常的宿主应用程序,以便利用它。 这就是不建议使用缓存(异常)的另一个原因。 设置主机应用程序基不同于沙盒应用程序基能减轻使用风险。

    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 parameter has the params 关键字,表示不必创建 StrongName 数组。 允许传递 0、1 或更多强名称作为参数。

    • 要创建应用程序域的代码是:

    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 用于获取部分信任的程序集中的方法的句柄。 句柄可用于通过一种使用最少权限的安全方式执行代码。

    请注意,在前面的代码中,完全信任权限的 Assert 在输出 SecurityException 之前。

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

请参见

概念

代码安全维护指南