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

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

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

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

Я сузил это до рамки, на которую нацелены: 1.x target.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". Это должен быть проект библиотеки Class Class Class.
  • Подпишите сборку; вы можете использовать новый ключ или тот же, что и для 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 несколько предлагает сообщения правила анализа кода
  • Без каких-либо атрибутов правила Code Analysis довольны - либо с помощью 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 вызвали эту проблему, следовательно, тег.)

Ответ 1

Согласно 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, она будет иметь значение IsSecurityTransparent для true и IsSecurityCritical для false даже если вы помечаете метод атрибутом SecurityCritical)

Ответ 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);
        }
    }
}