.net - c#序列化




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

背景: 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);
    }  
}
  • 创建另一个名为“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();
        }
    }
}

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

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

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

  • SecurityPermission 是由两篇不同的MS文章( firstsecond )推荐的,尽管有趣的是它们在显式/​​隐式接口实现方面做了不同的事情
  • SecurityCritical 是Noda Time目前所拥有的,也是 这个问题的答案所 暗示的
  • SecuritySafeCritical 在某种程度上由Code Analysis规则消息提示
  • 没有 任何 属性,代码分析规则很高兴 - 存在 SecurityPermissionSecurityCritical ,规则会告诉您删除属性 - 除非您有 AllowPartiallyTrustedCallers 。 在任何一种情况下建议都没有帮助。
  • Noda Time已经应用了 AllowPartiallyTrustedCallers ; 无论是否应用了属性,此处的示例都不起作用。

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

我完全承认在涉及.NET的这种安全方面时非常迷失。 那么我该如何定位.NET 4.5并允许我的类型实现 ISerializable 并仍然可以在.NET Fiddle等环境中使用?

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


接受的答案是如此令人信服,我几乎认为这不是一个错误。 但是在做了一些实验之后,我可以说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:可ISerializable的实现

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

  • 应用 InternalTypeReferenceAttribute
  • 尝试多次创建沙箱(请参阅代码)
  • 或者,如果所有测试用例一次执行,这不是第一个

否则,当您实例化 SerializableCriticalClassInheritance security rules violated while overriding member... 完全不合适的 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:具有安全关键成员的常规课程

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

[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 triumvirate属性的设计完全符合上面显示的用法。 对我来说,无论使用其安全关键成员的尝试如何,部分受信任的代码甚至无法实例化类型似乎完全是胡说八道。 但实际上是一个更大的废话(实际上是 安全漏洞 ),部分受信任的代码可以直接访问安全关键方法(参见 案例2 ),而对于透明方法,即使是完全受信任的域也是如此。

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

更新: 已为此问题创建了 开发人员社区票证


根据 second ,在.NET 4.0中基本上你不应该将 ISerializable 用于部分受信任的代码,而应该使用 ISafeSerializationData

引自 second

重要

在.NET Framework 4.0之前的版本中,使用GetObjectData完成部分受信任程序集中的自定义用户数据的序列化。 从版本4.0开始,该方法使用SecurityCriticalAttribute属性进行标记,该属性可防止在部分受信任的程序集中执行。 要解决此问题,请实现ISafeSerializationData接口。

所以可能不是你想要听到的,如果你需要它,但我不认为在保持使用 ISerializable 同时可以解决它(除了回到你说你不想要的 Level1 安全性)。

PS: ISafeSerializationData 文档声明它只是用于异常,但它似乎没有那么具体,你可能想试一试......我基本上不能用你的示例代码测试它(除了删除 ISerializable 工作,但你已经知道了... ...你必须看看 ISafeSerializationData 适合你。

PS2: SecurityCritical 属性不起作用,因为在部件信任模式下加载程序集时会忽略它( 在Level2安全性上 )。 您可以在示例代码中看到它,如果在调用它之前在 ExecuteUntrustedCode 调试 target 变量,即使使用 SecurityCritical 属性标记方法,它也会将 IsSecurityTransparenttrue 并将 IsSecurityCriticalfalse





code-access-security