如何在不违反继承安全规则的情况下在 .NET 4+ 中实现 ISerializable?

How can I implement ISerializable in .NET 4+ without violating inheritance security rules?

背景:Noda Time包含很多 可序列化结构。虽然我不喜欢二进制序列化,但我们 在 1.x 时间线中收到了许多支持它的请求。 我们通过实现 ISerializable 接口来支持它。

我们最近收到了一个 问题 野田的报告 时间 2.x 在 .NET 中失败 Fiddle。使用 Noda 的相同代码 时间 1.x 工作正常。抛出的异常是这样的:

Inheritance security rules violated while overriding member: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. Security accessibility of the overriding method must match the security accessibility of the method being overriden.

我已将其缩小到目标框架:1.x 目标 .NET 3.5(客户端配置文件); 2.x 以 .NET 4.5 为目标。他们有 PCL 与 .NET Core 和 项目文件结构,但看起来这是无关紧要的。

我已经在本地项目中重现了这个,但我没有 找到了解决办法。

在 VS2017 中重现的步骤:

代码:

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

代码:

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 项目给出了以下异常(为了便于阅读而重新格式化):

Unhandled Exception: System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation.
--->
System.TypeLoadException:
Inheritance security rules violated while overriding member:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
Security accessibility of the overriding method must match the security
accessibility of the method being overriden.

注释掉的属性显示了我尝试过的东西:

如果我将 [assembly: SecurityRules(SecurityRuleSet.Level1)] 添加到 UntrustedCode 程序集(并取消注释 AllowPartiallyTrustedCallers 属性),代码运行时不会出现异常,但我认为这是解决问题的糟糕解决方案可能会妨碍其他代码。

我完全承认在谈到这种事情时我很迷茫 .NET 的安全方面。那么我可以做什么来瞄准.NET 4.5和 但允许我的类型实现 ISerializable 并且仍然用于 .NET 等环境 Fiddle?

(虽然我的目标是 .NET 4.5,但我认为是 .NET 4.0 安全策略更改导致了问题,因此是标记。)

根据MSDN见:

How to Fix Violations?

To fix a violation of this rule, make the GetObjectData method visible and overridable and make sure all instance fields are included in the serialization process or explicitly marked with the NonSerializedAttribute attribute.

The following example fixes the two previous violations by providing an overrideable implementation of ISerializable.GetObjectData on the Book class and by providing an implementation of ISerializable.GetObjectData on the Library class.

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

根据the MSDN, in .NET 4.0 basically you should not use ISerializable for partially trusted code, and instead you should use ISafeSerializationData

引用自https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

Important

In versions previous to .NET Framework 4.0, serialization of custom user data in a partially trusted assembly was accomplished using the GetObjectData. Starting with version 4.0, that method is marked with the SecurityCriticalAttribute attribute which prevents execution in partially trusted assemblies. To work around this condition, implement the ISafeSerializationData interface.

所以如果你需要它可能不是你想听到的,但我认为在继续使用 ISerializable 的同时没有任何解决方法(除了回到 Level1 安全,你说你不想的)。

PS: ISafeSerializationData 文档说它只是为了例外,但它似乎并没有那么具体,你可能想试一试......我基本上可以用您的示例代码测试它(除了删除 ISerializable 之外,但您已经知道)...您必须看看 ISafeSerializationData 是否足够适合您。

PS2:SecurityCritical 属性不起作用,因为在部分信任模式下加载程序集时会忽略它(on Level2 安全性) .你可以在你的示例代码中看到它,如果你在调用它之前调试 ExecuteUntrustedCode 中的 target 变量,它会有 IsSecurityTransparenttrueIsSecurityCriticalfalse,即使您使用 SecurityCritical 属性标记该方法)

接受的答案是如此令人信服,以至于我几乎相信这不是一个错误。但是现在做了一些实验之后,我可以说 Level2 安全性完全是一团糟;至少,有些东西真的很可疑。

几天前,我在我的库中遇到了同样的问题。我很快创建了一个单元测试;但是,我无法重现我在 .NET Fiddle 中遇到的问题,而完全相同的代码“成功地”在控制台应用程序中引发了异常。最后我找到了两个奇怪的方法来解决这个问题。

TL;DR:事实证明,如果您在您的消费者项目中使用所用库的内部类型,那么部分受信任的代码将作为预期:它能够实例化一个 ISerializable 实现 (并且不能直接调用安全关键代码,但请参见下文)。或者,更可笑的是,如果第一次没有成功,您可以尝试重新创建沙箱...

但是让我们看一些代码。

ClassLibrary.dll:

让我们分开两种情况:一种是针对具有安全关键内容的常规 class,另一种是 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]

注意:该属性可以应用于任何地方。在我的例子中,它是在随机测试中的一个方法 class 花了我几天时间才找到的。

注意 2:如果您 运行 将所有测试方法放在一起,测试可能会通过。

测试骨架class:

[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 已应用
  • 多次尝试创建sandbox(见代码)
  • 或者,如果一次执行所有测试用例并且这不是第一个

否则,实例化SerializableCriticalClass.

时会出现完全不合适的Inheritance security rules violated while overriding member...异常
[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:常规 class 具有安全关键成员

测试在与第一个相同的条件下通过。但是,这里的问题完全不同:部分受信任的代码可能会直接访问安全关键成员

[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 class 使用,如果您订阅 SerializeObjectState 事件(为什么不是可覆盖的method?),那么状态最后也会被Exception.GetObjectData消耗掉。

AllowPartiallyTrustedCallers/SecurityCritical/SecuritySafeCritical 属性的三巨头正是为上面显示的用法而设计的。对我来说,部分受信任的代码甚至无法实例化类型,无论是否尝试使用其安全关键成员,这似乎完全是胡说八道。但这是一个更大的废话(实际上是 安全漏洞 )部分受信任的代码可以直接访问安全关键方法(参见 案例 2)而即使来自完全受信任的域,透明方法也是被禁止的。

因此,如果您的消费者项目是一个测试或另一个 well-known 程序集,那么可以完美地使用内部技巧。对于 .NET Fiddle 和其他 real-life 沙盒环境,唯一的解决方案是恢复到 SecurityRuleSet.Level1,直到 Microsoft 修复此问题。


更新: 已为问题创建 Developer Community ticket