Як я можу реалізувати ISerializable у .NET 4+, не порушуючи правил безпеки спадкування?


109

Передумови: 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". Це повинен бути проект бібліотеки Classic Desktop 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є те , що в даний час має Нод час, і то , що відповідь на це питання в пропонує
  • SecuritySafeCritical дещо пропонується повідомленнями правил аналізу коду
  • Без будь - яких атрибутів, правила аналізу коду щасливі - або SecurityPermissionчи SecurityCritical немає, правила сказати вам , щоб видалити атрибути - якщо ви не зробите є AllowPartiallyTrustedCallers. Виконання пропозицій в будь-якому випадку не допомагає.
  • До нього AllowPartiallyTrustedCallersзастосував Noda Time ; приклад тут не працює ні з застосованим атрибутом, ні без нього.

Код працює без винятку, якщо я додаю [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атрибут не працює, оскільки він ігнорується, коли збірка завантажується в режимі часткового довіри ( на рівні Level2 ). Ви можете побачити це на прикладі коду, якщо ви налагодити targetзмінну ExecuteUntrustedCodeпрямо перед викликом, він буде мати IsSecurityTransparentв trueі IsSecurityCriticalдо , falseнавіть якщо ви відзначаєте метод з SecurityCriticalатрибутом)


Ага - дякую за пояснення. Сором, виняток тут настільки оманливий. Треба буде розібратися, що робити ...
Джон Скіт

@JonSkeet Чесно кажучи, я б повністю відкинув бінарну серіалізацію ... але я розумію, що ваша база користувачів може не сподобається
Jcl

Я думаю, що нам доведеться це зробити - це означає перейти на v3.0. Але це має й інші переваги ... Мені потрібно проконсультуватися із спільнотою Noda Time.
Джон Скіт

12
@JonSkeet btw, якщо вас цікавить, у цій статті пояснюються відмінності між рівнем безпеки 1 та 2 рівня (і ЧОМУ це не працює)
Jcl

8

Прийнята відповідь настільки переконлива, що я майже повірив, що це не помилка. Але після деяких експериментів зараз я можу сказати, що безпека рівня 2 - це повний безлад; принаймні, щось справді рибне.

Пару днів тому я наткнувся на те саме питання зі своїми бібліотеками. Я швидко створив одиничний тест; однак я не міг відтворити проблему, яку зазнав у .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: Ідентифікаційна реалізація

Те саме питання, що і в питанні. Тест проходить, якщо

  • 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 для класу Бібліотека.

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.