Практическое руководство. Выполнение не вполне безопасного кода в изолированной среде

Изолирование в песочнице — это способ запуска кода в ограниченной среде безопасности, ограничивающей разрешения доступа, предоставленные коду. Например, если имеется управляемая библиотека, полученная из источника с неполным доверием, не следует запускать ее как полностью доверенную. Вместо этого необходимо поместить код в "песочницу", которая ограничивает разрешения кода потенциально необходимыми ему по мнению пользователя (например, 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, используемый для добавления сборки в массиве StrongName параметра fullTrustAssemblies в вызове метода CreateDomain. Чтобы разрешить выполнение кода с частичным доверием или предложить службы приложению с частичным доверием, нужно запустить размещающий класс как полностью доверенный. Далее показано, как читается строгое имя StrongName сборки.

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

    Сборки .NET Framework, такие как mscorlib и System.dll, не обязательно добавлять в список полностью доверенных сборок, так как они загружаются в таком виде из глобального кэша сборок.

  3. Инициализируйте параметр AppDomainSetup метода CreateDomain. С помощью этого параметра можно управлять многими настройками нового домена 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)
    

    Дополнительные сведения.

    • Это единственная перегрузка метода CreateDomain, принимающая в качестве параметра набор разрешений PermissionSet, т. е. единственная перегрузка, позволяющая загружать приложение с настройками частичного доверия.

    • Параметр evidence не используется для вычисления набора разрешений; он используется для идентификации другими компонентами .NET Framework.

    • Задание свойства ApplicationBase параметра info является обязательным для данной перегрузки.

    • Параметр 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. Вызовите метод ExecuteUntrustedCode в созданном экземпляре класса Sandboxer.

    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. Если не используется утверждение Assert, метод ToString исключения SecurityException обнаружит наличие в стеке кода с частичным доверием и ограничит возвращенные сведения. Если бы код с частичным доверием смог считать эти сведения, создалась бы угроза безопасности. Во избежание этого разрешение UIPermission не предоставляется. Утверждения полного доверия нужно использовать очень осторожно и только в том случае, если существует уверенность, что уровень разрешений кода с частичным доверием не повысится до уровня разрешений полностью доверенного кода. Как правило, не следует вызывать код без полного доверия в той же функции и после вызова утверждения полного доверия. Рекомендуется всегда отменять утверждение после завершения его использования.

Пример

В следующем примере реализуется процедура, описанная в предыдущем подразделе. В этом примере проект с именем Sandboxer в решении Visual Studio также содержит проект с именем UntrustedCode, реализующий класс UntrustedClass. В этом сценарии предполагается, что загружена библиотечная сборка, содержащая метод, который должен возвратить значение 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();
        }
    }
}

См. также

Основные понятия

Правила написания безопасного кода