代码之家  ›  专栏  ›  技术社区  ›  Jon Skeet

如何在中实现ISerializable。NET 4+而不违反继承安全规则?

  •  121
  • Jon Skeet  · 技术社区  · 7 年前

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

    我们最近收到 issue report 野田佳彦 时间2。x个 failing within .NET Fiddle . 使用野田佳彦的相同代码 时间1。x工作正常。引发的异常如下:

    重写成员时违反了继承安全规则: “点头时间。期间系统运行时。序列化。ISerializable。GetObjectData(System.Runtime.Serialization.SerializationInfo, 系统运行时。序列化。StreamingContext)“”。安全 重写方法的可访问性必须与安全性匹配 正在重写的方法的可访问性。

    我已经将其缩小到目标框架:1。x个 目标。NET 3.5(客户资料);2、x个目标。净额4.5。他们有 在支持PCL方面与。NET Core和 项目文件结构,但这似乎无关紧要。

    我已经设法在当地的一个项目中复制了这一点,但我没有 找到了解决方案。

    在VS2017中复制的步骤:

    • 创建新解决方案
    • 创建新的经典Windows控制台应用程序目标。净额 4.5.1. 我把它叫做“CodeRunner”。
    • 在“项目属性”中,转到“签名”并使用对程序集进行签名 一把新钥匙。取消选中密码要求,并使用任何密钥文件名。
    • 粘贴以下代码以替换 Program.cs . 这是一个 中代码的缩写版本 this Microsoft sample . 我保持了所有的路径不变,所以如果你想回到 更完整的代码,您不需要更改任何其他内容。

    代码:

    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。(这部分是为了模拟野田佳彦时间的情况, 部分是为了让代码分析愉快。)
    • 在中粘贴以下代码 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项目会出现以下异常(为了可读性而重新格式化):

    未处理的异常:系统。反射特例:
    调用的目标引发了异常。
    ---&燃气轮机;
    系统TypeLoadException(类型加载异常):
    重写成员时违反了继承安全规则:
    '不受信任的代码。自定义结构。系统运行时。序列化。ISerializable。GetObjectData(…)。
    重写方法的安全可访问性必须与安全性匹配
    正在重写的方法的可访问性。

    注释掉的属性显示了我尝试过的内容:

    • SecurityPermission 由两篇不同的MS文章推荐( first , second ),尽管 有趣的是,它们围绕显式/隐式接口实现做了不同的事情
    • SecurityCritical 是野田佳彦目前拥有的 this question's answer 建议
    • SecuritySafeCritical 代码分析规则消息有点建议
    • 没有 任何 属性、代码分析规则都很满意 SecurityPermission 安全关键 现在,规则告诉您删除属性-除非您 AllowPartiallyTrustedCallers . 无论哪种情况,遵循建议都没有帮助。
    • 野田佳彦时间 允许部分受信任的呼叫者 应用于it;无论是否应用属性,这里的示例都不起作用。

    如果我添加 [assembly: SecurityRules(SecurityRuleSet.Level1)] UntrustedCode 程序集(并取消对 允许部分受信任的呼叫者 属性),但我认为这是一个糟糕的问题解决方案,可能会妨碍其他代码。

    我完全承认在这种情况下我很迷茫 的安全方面。净额。那又怎么样 可以 我对目标做了什么。净额4.5和 但允许我的类型实现 ISerializable可序列化 仍在使用中 环境,如。网提琴?

    (虽然我的目标是.NET 4.5,但我相信是.NET 4.0安全策略的更改导致了这个问题,因此才有了标签。)

    3 回复  |  直到 5 年前
        1
  •  62
  •   Community CDub    5 年前

    根据 the 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:the ISafeSerializationData 文档中指出,这只是针对例外情况,但似乎并没有那么具体,您可能想尝试一下。。。我基本上不能用您的示例代码测试它(除了删除 ISerializable可序列化 工作,但你已经知道了)。。。你得看看 ISafeSerializationData 很适合你。

    PS2: SecurityCritical 属性不起作用,因为在部分信任模式下加载程序集时会忽略该属性( 在Level 2安全上 ). 如果调试 target 变量输入 ExecuteUntrustedCode 在调用它之前 IsSecurityTransparent true IsSecurityCritical false 即使您使用 安全关键 属性)

        2
  •  11
  •   Community CDub    5 年前

    公认的答案是如此令人信服,以至于我几乎相信这不是一个bug。但在做了一些实验之后,现在我可以说Level2安全性是一团乱麻;至少,有些事情真的很可疑。

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

    TL;博士 :事实证明 如果在使用者项目中使用已用库的内部类型,则部分受信任的代码将按预期工作:它能够实例化 ISerializable 实施 (并且不能直接调用安全关键代码,但请参见下文)。或者,更荒谬的是,如果沙盒第一次不起作用,您可以尝试再次创建沙盒。。。

    但让我们看看一些代码。

    类库。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 属性并准备测试项目:

    单元测试。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);
    }
    

    后记:

    当然,这并不能解决您的问题。网提琴。但是现在,如果不是框架中的bug,我会非常惊讶。

    我现在最大的问题是被接受的答案中引用的部分。他们怎么会说出这些胡说八道?这个 ISafeSerializationData 显然不是解决任何问题的方案:它是由基地独家使用的 Exception 类,如果您订阅 SerializeObjectState 事件(为什么这不是一个可重写的方法?),然后状态也将被 Exception.GetObjectData 最后

    这个 AllowPartiallyTrustedCallers / SecurityCritical / SecuritySafeCritical 三重属性的设计正好符合上面所示的用法。在我看来,一个部分受信任的代码甚至不能实例化一个类型,而不管是否尝试使用它的安全关键成员,这完全是胡说八道。但这是一个更大的胡说八道(a 安全漏洞 实际上)部分受信任的代码可以直接访问安全关键方法(请参见 案例2 )然而,即使是来自完全受信任域的透明方法也禁止这样做。

    因此,如果您的消费者项目是一个测试或另一个众所周知的组件,那么内部技巧可以完美地使用。对于NET FIDLE和其他现实生活中的沙盒环境唯一的解决方案是恢复到 SecurityRuleSet.Level1 直到Microsoft修复此问题。


    更新: A. Developer Community ticket 已为此问题创建。

        3
  •  2
  •   Community CDub    5 年前

    根据 MSDN 请参见:

    如何修复违规行为?

    要修复违反此规则的情况,请使 GetObjectData 方法可见且可重写,并确保所有实例字段都包含在序列化过程中,或显式标记为 NonSerializedAttribute 属性

    以下内容 example 通过提供ISerializable的可重写实现,修复了前面的两个冲突。通过提供ISerializable的实现,获取Book类上的ObjectData。库类上的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);
            }
        }
    }