Как я могу реализовать ISerializable в .NET 4+ без нарушения правил безопасности наследования?


110

Предпосылки: Noda Time содержит множество сериализуемых структур. Хотя мне не нравится двоичная сериализация, мы получили много запросов на ее поддержку еще на временной шкале 1.x. Мы поддерживаем это реализацией ISerializableинтерфейса.

Мы получили недавний отчет о сбое Noda Time 2.x в .NET Fiddle . Тот же код, использующий Noda Time 1.x, работает нормально. Возникло следующее исключение:

Правила безопасности наследования нарушены при переопределении члена: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. Доступность с точки зрения безопасности переопределяемого метода должна соответствовать доступности с точки зрения безопасности переопределяемого метода.

Я сузил это до целевой платформы: 1.x нацелен на .NET 3.5 (профиль клиента); 2.x нацелен на .NET 4.5. У них большие различия в плане поддержки PCL и .NET Core и файловой структуры проекта, но похоже, что это не имеет значения.

Мне удалось воспроизвести это в локальном проекте, но я не нашел решения.

Шаги для воспроизведения в VS2017:

  • Создать новое решение
  • Создайте новое классическое консольное приложение Windows, ориентированное на .NET 4.5.1. Я назвал его «CodeRunner».
  • В свойствах проекта перейдите в раздел Подписывание и подпишите сборку новым ключом. Снимите флажок с требования к паролю и используйте любое имя файла ключей.
  • Вставьте следующий код для замены Program.cs. Это сокращенная версия кода в этом примере Microsoft . Я сохранил все пути одинаковыми, поэтому, если вы хотите вернуться к более полному коду, вам не нужно ничего менять.

Код:

using System;
using System.Security;
using System.Security.Permissions;

class Sandboxer : MarshalByRefObject  
{  
    static void Main()  
    {  
        var adSetup = new AppDomainSetup();  
        adSetup.ApplicationBase = System.IO.Path.GetFullPath(@"..\..\..\UntrustedCode\bin\Debug");  
        var permSet = new PermissionSet(PermissionState.None);  
        permSet.AddPermission(new SecurityPermission(SecurityPermissionFlag.Execution));  
        var fullTrustAssembly = typeof(Sandboxer).Assembly.Evidence.GetHostEvidence<System.Security.Policy.StrongName>();  
        var newDomain = AppDomain.CreateDomain("Sandbox", null, adSetup, permSet, fullTrustAssembly);  
        var handle = Activator.CreateInstanceFrom(  
            newDomain, typeof(Sandboxer).Assembly.ManifestModule.FullyQualifiedName,  
            typeof(Sandboxer).FullName  
            );  
        Sandboxer newDomainInstance = (Sandboxer) handle.Unwrap();  
        newDomainInstance.ExecuteUntrustedCode("UntrustedCode", "UntrustedCode.UntrustedClass", "IsFibonacci", new object[] { 45 });  
    }  

    public void ExecuteUntrustedCode(string assemblyName, string typeName, string entryPoint, Object[] parameters)  
    {  
        var target = System.Reflection.Assembly.Load(assemblyName).GetType(typeName).GetMethod(entryPoint);
        target.Invoke(null, parameters);
    }  
}
  • Создайте еще один проект под названием «UntrustedCode». Это должен быть проект библиотеки классов классического рабочего стола.
  • Подпишите сборку; вы можете использовать новый ключ или тот же, что и для CodeRunner. (Это частично для имитации ситуации с Noda Time, а частично для того, чтобы сделать анализ кода счастливым.)
  • Вставьте следующий код Class1.cs(перезаписав то, что там есть):

Код:

using System;
using System.Runtime.Serialization;
using System.Security;
using System.Security.Permissions;

// [assembly: AllowPartiallyTrustedCallers]

namespace UntrustedCode
{
    public class UntrustedClass
    {
        // Method named oddly (given the content) in order to allow MSDN
        // sample to run unchanged.
        public static bool IsFibonacci(int number)
        {
            Console.WriteLine(new CustomStruct());
            return true;
        }
    }

    [Serializable]
    public struct CustomStruct : ISerializable
    {
        private CustomStruct(SerializationInfo info, StreamingContext context) { }

        //[SecuritySafeCritical]
        //[SecurityCritical]
        //[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            throw new NotImplementedException();
        }
    }
}

При запуске проекта CodeRunner возникает следующее исключение (переформатировано для удобства чтения):

Необработанное исключение: System.Reflection.TargetInvocationException:
исключение было выброшено целью вызова.
--->
System.TypeLoadException:
правила безопасности наследования нарушены при переопределении члена:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
Доступность с точки зрения безопасности переопределяемого метода должна соответствовать
доступности с точки зрения безопасности переопределяемого метода.

Закомментированные атрибуты показывают то, что я пробовал:

  • SecurityPermissionрекомендуется двумя разными статьями MS ( первая , вторая ), хотя, что интересно, они делают разные вещи в отношении явной / неявной реализации интерфейса
  • SecurityCriticalэто то, что сейчас есть у Noda Time, и это то, что предлагает ответ на этот вопрос
  • SecuritySafeCritical несколько подсказывают сообщения правил анализа кода
  • Без каких-либо атрибутов правила анализа кода удовлетворены - с любым из них SecurityPermissionили при их SecurityCritical наличии, правила предписывают вам удалить атрибуты - если у вас их нет AllowPartiallyTrustedCallers. Следование предложениям в любом случае не помогает.
  • Noda Time AllowPartiallyTrustedCallersприменил к нему; приведенный здесь пример не работает ни с примененным атрибутом, ни без него.

Код запускается без исключения, если я добавляю [assembly: SecurityRules(SecurityRuleSet.Level1)]в UntrustedCodeсборку (и раскомментирую AllowPartiallyTrustedCallersатрибут), но я считаю, что это плохое решение проблемы, которая может затруднить работу другого кода.

Я полностью признаю, что сильно заблудился, когда дело доходит до такого рода аспектов безопасности .NET. Так что можно сделать , чтобы обеспечить работу с .NET 4.5 и все же позволяют мои типы реализовать ISerializableи по- прежнему использоваться в средах , таких как .NET Fiddle?

(Хотя я нацелен на .NET 4.5, я считаю, что проблема была вызвана изменениями политики безопасности .NET 4.0, отсюда и тег.)


Достаточно интересно, что это объяснение изменений модели безопасности в 4.0 предполагает, что простое удаление AllowPartiallyTrustedCallersдолжно помочь, но, похоже, это не имеет значения,
Матиас Р. Джессен

Ответы:


56

Согласно MSDN , в .NET 4.0 в основном вы не должны использовать ISerializableчастично доверенный код, вместо этого вы должны использовать ISafeSerializationData

Цитата из https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

Важный

В версиях, предшествующих .NET Framework 4.0, сериализация пользовательских данных в частично доверенной сборке выполнялась с помощью GetObjectData. Начиная с версии 4.0, этот метод помечен атрибутом SecurityCriticalAttribute, который предотвращает выполнение в частично доверенных сборках. Чтобы обойти это условие, реализуйте интерфейс ISafeSerializationData.

Так что, вероятно, это не то, что вы хотели услышать, если вам это нужно, но я не думаю, что есть какой-то способ обойти это, продолжая использовать ISerializable(кроме возврата к Level1безопасности, которую вы сказали, что не хотите).

PS: в ISafeSerializationDataдокументации указано, что это только для исключений, но это не кажется таким уж конкретным, вы можете захотеть дать ему шанс ... Я в основном не могу проверить его с помощью вашего образца кода (кроме удаления ISerializableработ, но вы это уже знали) ... вам нужно посмотреть, ISafeSerializationDataподходит ли вам достаточно.

PS2: SecurityCriticalатрибут не работает, потому что он игнорируется, когда сборка загружается в режиме частичного доверия ( на уровне безопасности 2 ). Вы можете увидеть это на примере кода, если вы отладить targetпеременную ExecuteUntrustedCodeпрямо перед вызовом, он будет иметь IsSecurityTransparentв trueи IsSecurityCriticalк , falseдаже если вы отмечаете метод с SecurityCriticalатрибутом)


Ага - спасибо за объяснение. Жаль, что это исключение вводит в заблуждение. Нужно будет решить, что делать ...
Джон Скит,

@JonSkeet Честно говоря, я бы вообще отказался от двоичной сериализации ... но я понимаю, что вашей пользовательской
базе

Думаю, нам придется это сделать - что означает переход на v3.0. Хотя у него есть и другие преимущества ... Мне нужно проконсультироваться с сообществом Noda Time.
Джон Скит

12
@JonSkeet Кстати, если вам интересно, в этой статье объясняются различия между безопасностью уровня 1 и уровня 2 (и ПОЧЕМУ это не работает)
Jcl

8

Принятый ответ настолько убедителен, что я почти поверил, что это не ошибка. Но после некоторых экспериментов я могу сказать, что безопасность Level2 - это полный беспорядок; по крайней мере, что-то действительно подозрительное.

Пару дней назад я столкнулся с той же проблемой с моими библиотеками. Я быстро создал модульный тест; однако мне не удалось воспроизвести проблему, с которой я столкнулся в .NET Fiddle, в то время как тот же самый код «успешно» выдал исключение в консольном приложении. В конце концов я нашел два странных способа решить эту проблему.

TL; DR : Оказывается, что если вы используете внутренний тип используемой библиотеки в своем потребительском проекте, то частично доверенный код работает так, как ожидалось: он может создать экземпляр ISerializableреализации (а критический для безопасности код не может быть вызван напрямую, но см. ниже). Или, что еще более смешно, вы можете попробовать создать песочницу снова, если она не сработала в первый раз ...

Но давайте посмотрим код.

ClassLibrary.dll:

Разделяем два случая: один для обычного класса с критически важным для безопасности контентом и второй вариант ISerializableреализации:

public class CriticalClass
{
    public void SafeCode() { }

    [SecurityCritical]
    public void CriticalCode() { }

    [SecuritySafeCritical]
    public void SafeEntryForCriticalCode() => CriticalCode();
}

[Serializable]
public class SerializableCriticalClass : CriticalClass, ISerializable
{
    public SerializableCriticalClass() { }

    private SerializableCriticalClass(SerializationInfo info, StreamingContext context) { }

    [SecurityCritical]
    public void GetObjectData(SerializationInfo info, StreamingContext context) { }
}

Один из способов решить эту проблему - использовать внутренний тип из сборки потребителя. Любой тип сделает это; теперь я определяю атрибут:

[AttributeUsage(AttributeTargets.All)]
internal class InternalTypeReferenceAttribute : Attribute
{
    public InternalTypeReferenceAttribute() { }
}

И соответствующие атрибуты, примененные к сборке:

[assembly: InternalsVisibleTo("UnitTest, PublicKey=<your public key>")]
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityRules(SecurityRuleSet.Level2, SkipVerificationInFullTrust = true)]

Подпишите сборку, примените ключ к InternalsVisibleToатрибуту и ​​подготовьтесь к тестированию проекта:

UnitTest.dll (использует NUnit и ClassLibrary):

Чтобы использовать внутренний трюк, тестовая сборка также должна быть подписана. Атрибуты сборки:

// Just to make the tests security transparent by default. This helps to test the full trust behavior.
[assembly: AllowPartiallyTrustedCallers] 

// !!! Comment this line out and the partial trust test cases may fail for the fist time !!!
[assembly: InternalTypeReference]

Примечание : атрибут можно применять где угодно. В моем случае это был метод из случайного тестового класса, на поиск которого у меня ушло несколько дней.

Примечание 2 : Если вы запустите все методы тестирования вместе, может случиться так, что тесты пройдут.

Скелет тестового класса:

[TestFixture]
public class SecurityCriticalAccessTest
{
    private partial class Sandbox : MarshalByRefObject
    {
    }

    private static AppDomain CreateSandboxDomain(params IPermission[] permissions)
    {
        var evidence = new Evidence(AppDomain.CurrentDomain.Evidence);
        var permissionSet = GetPermissionSet(permissions);
        var setup = new AppDomainSetup
        {
            ApplicationBase = AppDomain.CurrentDomain.BaseDirectory,
        };

        var assemblies = AppDomain.CurrentDomain.GetAssemblies();
        var strongNames = new List<StrongName>();
        foreach (Assembly asm in assemblies)
        {
            AssemblyName asmName = asm.GetName();
            strongNames.Add(new StrongName(new StrongNamePublicKeyBlob(asmName.GetPublicKey()), asmName.Name, asmName.Version));
        }

        return AppDomain.CreateDomain("SandboxDomain", evidence, setup, permissionSet, strongNames.ToArray());
    }

    private static PermissionSet GetPermissionSet(IPermission[] permissions)
    {
        var evidence = new Evidence();
        evidence.AddHostEvidence(new Zone(SecurityZone.Internet));
        var result = SecurityManager.GetStandardSandbox(evidence);
        foreach (var permission in permissions)
            result.AddPermission(permission);
        return result;
    }
}

И давайте посмотрим тестовые примеры один за другим

Случай 1: реализация ISerializable

Та же проблема, что и в вопросе. Тест пройден, если

  • InternalTypeReferenceAttribute применяется
  • песочницу пытаются создать несколько раз (см. код)
  • или, если все тестовые примеры выполняются сразу и это не первый

В противном случае при создании Inheritance security rules violated while overriding member...экземпляра возникает совершенно неприемлемое исключение SerializableCriticalClass.

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void SerializableCriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestSerializableCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new SecurityPermission(SecurityPermissionFlag.SerializationFormatter), // BinaryFormatter
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestSerializableCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestSerializableCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // ISerializable implementer can be created.
        // !!! May fail for the first try if the test does not use any internal type of the library. !!!
        var critical = new SerializableCriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
        Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, new StreamingContext()));

        // BinaryFormatter calls the critical method via a safe route (SerializationFormatter permission is required, though)
        new BinaryFormatter().Serialize(new MemoryStream(), critical);
    }

}

Случай 2: Обычный класс с важными для безопасности членами

Тест проходит в тех же условиях, что и первый. Однако здесь проблема совершенно в другом: код с частичным доверием может напрямую обращаться к критически важному элементу безопасности .

[Test]
[SecuritySafeCritical] // for Activator.CreateInstance
public void CriticalClass_PartialTrustAccess()
{
    var domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess), // Assert.IsFalse
        new EnvironmentPermission(PermissionState.Unrestricted)); // Assert.Throws (if fails)
    var handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    var sandbox = (Sandbox)handle.Unwrap();
    try
    {
        sandbox.TestCriticalClass();
        return;
    }
    catch (Exception e)
    {
        // without [InternalTypeReference] it may fail for the first time
        Console.WriteLine($"1st try failed: {e.Message}");
    }

    domain = CreateSandboxDomain(
        new ReflectionPermission(ReflectionPermissionFlag.MemberAccess)); // Assert.IsFalse
    handle = Activator.CreateInstance(domain, Assembly.GetExecutingAssembly().FullName, typeof(Sandbox).FullName);
    sandbox = (Sandbox)handle.Unwrap();
    sandbox.TestCriticalClass();

    Assert.Inconclusive("Meh... succeeded only for the 2nd try");
}

private partial class Sandbox
{
    public void TestCriticalClass()
    {
        Assert.IsFalse(AppDomain.CurrentDomain.IsFullyTrusted);

        // A type containing critical methods can be created
        var critical = new CriticalClass();

        // Critical method can be called via a safe method
        critical.SafeEntryForCriticalCode();

        // Critical method cannot be called directly by a transparent method
        // !!! May fail for the first time if the test does not use any internal type of the library. !!!
        // !!! Meaning, a partially trusted code has more right than a fully trusted one and is       !!!
        // !!! able to call security critical method directly.                                        !!!
        Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    }
}

Случай 3-4: варианты случая 1-2 с полным доверием

Для полноты картины здесь представлены те же случаи, что и описанные выше, выполненные в полностью доверенном домене. Если вы удалите [assembly: AllowPartiallyTrustedCallers]тесты, они не пройдут, потому что тогда вы сможете получить доступ к критическому коду напрямую (поскольку методы больше не прозрачны по умолчанию).

[Test]
public void CriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // A type containing critical methods can be created
    var critical = new CriticalClass();

    // Critical method cannot be called directly by a transparent method
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();
}

[Test]
public void SerializableCriticalClass_FullTrustAccess()
{
    Assert.IsTrue(AppDomain.CurrentDomain.IsFullyTrusted);

    // ISerializable implementer can be created
    var critical = new SerializableCriticalClass();

    // Critical method cannot be called directly by a transparent method (see also AllowPartiallyTrustedCallersAttribute)
    Assert.Throws<MethodAccessException>(() => critical.CriticalCode());
    Assert.Throws<MethodAccessException>(() => critical.GetObjectData(null, default(StreamingContext)));

    // Critical method can be called via a safe method
    critical.SafeEntryForCriticalCode();

    // BinaryFormatter calls the critical method via a safe route
    new BinaryFormatter().Serialize(new MemoryStream(), critical);
}

Эпилог:

Конечно, это не решит вашу проблему с .NET Fiddle. Но сейчас я был бы очень удивлен, если бы это не ошибка фреймворка.

Самый большой вопрос для меня сейчас - это цитируемая часть принятого ответа. Как они вышли с этой чушью? ISafeSerializationDataЯвно не подходит ни для чего: он используется исключительно базовым Exceptionклассом , и если вы подписались на SerializeObjectStateсобытие (почему не о том , что переопределение метод?), То состояние также будет потребляться Exception.GetObjectDataв конце концов.

AllowPartiallyTrustedCallers/ SecurityCritical/ SecuritySafeCriticalТриумвират атрибутов были разработаны именно для использования указанного выше. Мне кажется полной ерундой, что частично доверенный код не может даже создать экземпляр типа, независимо от попытки использования его критически важных для безопасности элементов. Но это еще большая ерунда (на самом деле дыра в безопасности ), что частично доверенный код может напрямую обращаться к критически важному для безопасности методу (см. Случай 2 ), тогда как это запрещено для прозрачных методов даже из полностью доверенного домена.

Так что если ваш потребительский проект представляет собой тестовую или другую известную сборку, то внутренний трюк можно использовать отлично. Для .NET Fiddle и других реальных изолированных сред единственное решение - вернуться к прежним версиям, SecurityRuleSet.Level1пока это не будет исправлено Microsoft.


Обновление: Сообщество разработчиков билет был создан для выпуска.


2

Согласно MSDN см .:

Как исправить нарушения?

Чтобы устранить нарушение этого правила, сделайте метод GetObjectData видимым и переопределяемым и убедитесь, что все поля экземпляра включены в процесс сериализации или явно помечены атрибутом NonSerializedAttribute .

В следующем примере исправляются два предыдущих нарушения, предоставляя переопределяемую реализацию ISerializable.GetObjectData в классе Book и предоставляя реализацию ISerializable.GetObjectData в классе Library.

using System;
using System.Security.Permissions;
using System.Runtime.Serialization;

namespace Samples2
{
    [Serializable]
    public class Book : ISerializable
    {
        private readonly string _Title;

        public Book(string title)
        {
            if (title == null)
                throw new ArgumentNullException("title");

            _Title = title;
        }

        protected Book(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            _Title = info.GetString("Title");
        }

        public string Title
        {
            get { return _Title; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected virtual void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue("Title", _Title);
        }

        [SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.SerializationFormatter)]
        void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
        {
            if (info == null)
                throw new ArgumentNullException("info");

            GetObjectData(info, context);
        }
    }

    [Serializable]
    public class LibraryBook : Book
    {
        private readonly DateTime _CheckedOut;

        public LibraryBook(string title, DateTime checkedOut)
            : base(title)
        {
            _CheckedOut = checkedOut;
        }

        protected LibraryBook(SerializationInfo info, StreamingContext context)
            : base(info, context)
        {
            _CheckedOut = info.GetDateTime("CheckedOut");
        }

        public DateTime CheckedOut
        {
            get { return _CheckedOut; }
        }

        [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)]
        protected override void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            base.GetObjectData(info, context);

            info.AddValue("CheckedOut", _CheckedOut);
        }
    }
}

2
Статья, на которую вы ссылаетесь, предназначена для CA2240, который не запущен - код не нарушает его. Это структура, поэтому она эффективно запечатана; у него нет полей; он реализуется GetObjectDataявно, но неявно это не помогает.
Джон Скит

15
Конечно, и спасибо за попытку, но я объясняю, почему это не работает. (И в качестве рекомендации - для чего-то такого сложного, где вопрос включает проверяемый пример, неплохо попробовать применить предложенное исправление и посмотреть, действительно ли оно помогает.)
Джон Скит,
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.