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

时间:2018-01-20 11:29:27

标签: .net serialization .net-4.0 code-access-security

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

我们最近收到了Noda的issue report 时间2.x failing within .NET Fiddle。使用Noda的相同代码 时间1.x工作正常。抛出的异常是:

  

覆盖成员时违反了继承安全规则:   ' NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo,   System.Runtime.Serialization.StreamingContext)&#39 ;.安全   覆盖方法的可访问性必须与安全性相匹配   被覆盖的方法的可访问性。

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

我设法在当地项目中重现了这一点,但我还没有 找到了解决方案。

在VS2017中重现的步骤:

  • 创建新解决方案
  • 创建一个面向.NET的新经典Windows控制台应用程序 4.5.1。我打电话给#34; 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);
    }  
}
  • 创建另一个名为&#34; UntrustedCode&#34;的项目。这应该是一个 经典桌面类库项目。
  • 签署组装;您可以使用新密钥或相同的密钥 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:
  调用目标引发了异常   ---&GT;
  System.TypeLoadException:
  覆盖成员时违反了继承安全规则:
  &#39; UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...)
  覆盖方法的安全可访问性必须与安全性相匹配   被覆盖的方法的可访问性。

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

    两篇不同的MS文章(first)推荐
  • SecurityPermissionsecond),尽管如此 有趣的是,他们围绕显式/隐式接口实现做了不同的事情
  • SecurityCritical是Noda Time目前拥有的,this question's answer建议
  • 代码分析规则消息稍微建议
  • SecuritySafeCritical
  • 没有任何属性,代码分析规则很高兴 - 使用SecurityPermissionSecurityCritical 目前,规则告诉您删除属性 - 除非您AllowPartiallyTrustedCallers。在这两种情况下提出建议都没有帮助。
  • 野田时间已适用AllowPartiallyTrustedCallers;这里的示例在使用或不应用属性的情况下都不起作用。

如果我将[assembly: SecurityRules(SecurityRuleSet.Level1)]添加到UntrustedCode程序集(并取消注释AllowPartiallyTrustedCallers属性),代码会毫无例外地运行,但我相信这个问题解决不好这可能会妨碍其他代码。

我完全承认在谈到这种情况时会非常迷失 .NET的安全方面。那么可以我做的目标是.NET 4.5和 允许我的类型实现ISerializable并仍在使用中 .NET Fiddle等环境?

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

3 个答案:

答案 0 :(得分:44)

根据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的同时可以解决它(除了回到{{1安全性,你说你不想)。

PS:Level1文档声明它仅用于例外,但它看起来并不是那么具体,你可能想试一试......我基本上不能这样做用你的示例代码测试它(除了删除ISafeSerializationData之外的工作,但你已经知道了)......你必须看看ISerializable是否适合你。

PS2:ISafeSerializationData属性不起作用,因为在部分信任模式( on Level2 security )上加载程序集时会忽略它。您可以在示例代码中看到它,如果您在调用它之前调试SecurityCritical中的target变量,它会ExecuteUntrustedCodeIsSecurityTransparent和{{ 1}}到true,即使您使用IsSecurityCritical属性标记方法

答案 1 :(得分:2)

根据MSDN见:

  

如何修复违规行为?

     

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

     

以下example通过在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 :(得分:1)

被接受的答案令人信服,以至于我几乎相信这不是一个错误。但是,经过一些实验之后,我可以说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.Level1,直到Microsoft修复此问题为止。


更新:为此问题创建了一个Developer Community ticket