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


109

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

我们已经收到了有关.NET Fiddle中Noda Time 2.x 的最新问题报告。使用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中重现的步骤:

  • 创建一个新的解决方案
  • 创建一个针对.NET 4.5.1的新的经典Windows控制台应用程序。我称它为“ 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(...)。
覆盖方法的安全
可访问性必须与被覆盖方法的安全可访问性相匹配。

注释掉的属性显示了我尝试过的事情:

  • SecurityPermissionMS推荐了两篇不同的MS文章(第一篇, 第二篇),尽管有趣的是,它们围绕显式/隐式接口实现做了不同的事情
  • SecurityCritical是Noda Time当前所拥有的,这个问题的答案表明了什么
  • SecuritySafeCritical 由代码分析规则消息建议
  • 没有任何属性,“代码分析”规则会很满意-不管是 存在SecurityPermission还是SecurityCritical存在,规则都会告诉您删除属性-除非您确实AllowPartiallyTrustedCallers。无论哪种情况,遵循建议都无济于事。
  • Noda Time已AllowPartiallyTrustedCallers应用;无论是否应用属性,此处的示例均不起作用。

如果我将其添加[assembly: SecurityRules(SecurityRuleSet.Level1)]UntrustedCode程序集中(并取消注释该AllowPartiallyTrustedCallers属性),则代码会无例外地运行,但是我认为这是一个很糟糕的解决方案,可以阻止其他代码。

我完全承认,在涉及.NET的这种安全方面时,我已经迷失了。那么什么可以做些什么来面向.NET 4.5,但让我的类型来实现ISerializable,并在环境,如.NET小提琴仍然可以使用?

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


有趣的是,这种对4.0中安全模型更改的解释表明,只需删除即可解决问题AllowPartiallyTrustedCallers,但这似乎没有什么不同
Mathias R. Jessen

Answers:


56

根据MSDN,基本上在.NET 4.0中,您不应使用ISerializable部分受信任的代码,而应使用ISafeSerializationData

https://docs.microsoft.com/zh-cn/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调用前正确的,它必须IsSecurityTransparenttrueIsSecurityCriticalfalse即使你用标记的方法SecurityCritical属性)


啊哈-感谢您的解释。可惜异常在这里是如此令人误解。将需要解决该怎么做...
乔恩·斯基特

@JonSkeet老实说,我会一起放弃二进制序列化...但是我知道您的用户群可能不喜欢
Jcl

我认为我们必须这样做-这意味着要迁移到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:可序列化的实现

与问题中的问题相同。测试是否通过

  • 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.Level1Microsoft修复的解决方案。


更新:一个开发者社区车票已经为这个问题产生。


2

根据MSDN看:

如何解决违规行为?

若要解决违反此规则的问题,请使GetObjectData方法可见且可重写,并确保所有实例字段都包含在序列化过程中或用NonSerializedAttribute属性显式标记。

下面的示例通过在Book类上提供ISerializable.GetObjectData的可重写实现,并在Library类上提供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
当然,感谢您的尝试-但我正在解释为什么它不起作用。(并且作为建议-对于类似这样棘手的问题,其中的问题包括一个可验证的示例,建议您尝试应用建议的修复程序,看看是否有实际帮助。)
Jon Skeet
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.