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

Jon*_*eet 96 .net c# serialization appdomain code-access-security

背景: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中重现的步骤:

  • 创建新的解决方案
  • 创建一个面向.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);
    }  
}
Run Code Online (Sandbox Code Playgroud)
  • 创建另一个名为"UntrustedCode"的项目.这应该是经典桌面类库项目.
  • 签署组件; 您可以使用新密钥或与CodeRunner相同的密钥.(这部分是为了模仿Noda Time的情况,部分是为了让Code Analysis感到满意.)
  • 将以下代码粘贴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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

运行CodeRunner项目会出现以下异常(为了便于阅读而重新格式化):

未处理的异常:System.Reflection.TargetInvocationException:
调用的目标抛出了异常.
--->
System.TypeLoadException:
覆盖成员时违反了继承安全规则:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
覆盖方法的安全性
可访问性必须与被覆盖的方法的安全性可访问性相匹配.

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

  • SecurityPermission由两篇不同的MS文章(第一, 第二篇)推荐,尽管有趣的是它们在显式/​​隐式接口实现方面做了不同的事情
  • SecurityCritical是Noda Time目前所拥有的,这个问题的答案所暗示的
  • SecuritySafeCritical 有点建议使用代码分析规则消息
  • 没有任何属性,代码分析规则很高兴-无论使用哪种SecurityPermissionSecurityCritical 目前,该规则告诉你删除的属性-除非你AllowPartiallyTrustedCallers.在任何一种情况下建议都没有帮助.
  • Noda Time AllowPartiallyTrustedCallers适用于它; 无论是否应用了属性,此处的示例都不起作用.

如果我添加[assembly: SecurityRules(SecurityRuleSet.Level1)]UntrustedCode程序集(并取消注释AllowPartiallyTrustedCallers属性),代码运行没有异常,但我相信这是一个很差的解决方案,可能会妨碍其他代码.

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

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

Jcl*_*Jcl 48

根据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调用前正确的,它必须IsSecurityTransparenttrueIsSecurityCriticalfalse即使你用标记的方法SecurityCritical属性)

  • @JonSkeet btw,如果你有兴趣,[本文](https://blogs.msdn.microsoft.com/shawnfa/2009/11/12/differences-between-the-security-rule-sets/)解释了这些差异级别1和级别2之间的安全性(为什么它不起作用) (12认同)

Gyö*_*zeg 10

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

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

TL;DR:事实证明,如果您在消费者项目中使用所用库的内部类型,那么部分受信任的代码会按预期工作:它能够实例化一个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) { }
}
Run Code Online (Sandbox Code Playgroud)

解决该问题的一种方法是使用来自消费者程序集的内部类型。任何类型都可以;现在我定义一个属性:

[AttributeUsage(AttributeTargets.All)]
internal class InternalTypeReferenceAttribute : Attribute
{
    public InternalTypeReferenceAttribute() { }
}
Run Code Online (Sandbox Code Playgroud)

以及应用于程序集的相关属性:

[assembly: InternalsVisibleTo("UnitTest, PublicKey=<your public key>")]
[assembly: AllowPartiallyTrustedCallers]
[assembly: SecurityRules(SecurityRuleSet.Level2, SkipVerificationInFullTrust = true)]
Run Code Online (Sandbox Code Playgroud)

对程序集进行签名,将密钥应用到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]
Run Code Online (Sandbox Code Playgroud)

注意:该属性可以应用于任何地方。就我而言,它是在随机测试类中的一种方法上,我花了几天时间才找到。

注意 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;
    }
}
Run Code Online (Sandbox Code Playgroud)

让我们一一看看测试用例

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

}
Run Code Online (Sandbox Code Playgroud)

案例 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());
    }
}
Run Code Online (Sandbox Code Playgroud)

案例 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);
}
Run Code Online (Sandbox Code Playgroud)

结语:

当然,这不会解决您使用 .NET Fiddle 的问题。但是现在,如果它不是框架中的错误,我会感到非常惊讶。

现在对我来说最大的问题是已接受答案中引用的部分。他们是怎么搞出这种胡说八道的?TheISafeSerializationData显然不是任何解决方案:它仅由基Exception类使用,如果您订阅该SerializeObjectState事件(为什么这不是可覆盖的方法?),那么最终也将使用状态Exception.GetObjectData

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

所以如果你的消费者项目是一个测试或者其他知名的程序集,那么内部的技巧就可以完美的使用。对于 .NET Fiddle 和其他现实生活中的沙盒环境,唯一的解决方案是恢复到SecurityRuleSet.Level1Microsoft 修复此问题之前。


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