如何在.NET4 + 中实现 ISerializer 而不违反继承安全规则?

背景: 野田时光包含许多 serializable structs. While I dislike binary serialization, we 回到1.x 时间线,收到了许多支持它的请求。 我们通过实现 ISerializable接口来支持它。

我们最近收到了一个问题 野田佳彦的报告 时间2.x < a href = “ https://dotnetfiddle.NET/vTCzMJ”rel = “ noReferrer”> 在.NET 中失败 Fiddle . 使用 Noda 的相同代码 Time 1.x 运行良好,抛出的异常如下:

重写成员时违反了继承安全规则: ‘ NodaTime.Duration.System.Runtime.Serialization.ISerialization.GetObjectData (System.Runtime.Serialization. SerializationInfo, System.Runtime.Serialization. StreamingContext)’。安全性 重写方法的可访问性必须与安全性匹配 被重写方法的可访问性。

我已经把范围缩小到目标框架: 1.x 目标.NET 3.5(客户端配置文件) ; 2.x 目标.NET 4.5 在支持 PCL 和.NET 核心以及 项目文件结构,但看起来这是无关紧要的。

我设法在当地的一个项目中复制了这个,但是我没有 找到了解决办法。

Steps to reproduce in VS2017:

  • 创建新的解决方案
  • 创建一个针对.NET 的新的经典 Windows 控制台应用 4.5.1. 我叫它“ CodeRunner”。
  • 在项目属性中,转到“签名”并使用 取消密码要求,并使用任何密钥文件名。
  • 粘贴以下代码以替换 Program.cs 在 < a href = “ https://Learn.Microsoft.com/en-us/dotnet/Framework/misc/how-to-run-part-trust-code-in-a-sandbox # example”rel = “ norefrer”> this Microsoft 样本 。 I've kept all the paths the same, so if you want to go back to the 更完整的代码,您不应该需要更改任何其他代码。

Code:

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);
}
}
  • 创建另一个名为“ Untrust dCode”的项目 经典桌面类库项目。
  • 为程序集签名; 可以使用新密钥,也可以使用与 (这部分是为了模拟 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 项目会出现以下异常(为了便于阅读而重新格式化) :

未处理的异常: 系统、反射、 TargetInvocationException:
Exception has been thrown by the target of an invocation.
-
System.TypeLoadException:
重写成员时违反了继承安全规则:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
重写方法的安全可访问性必须与安全性匹配
被重写方法的可访问性。

注释掉的属性显示了我试过的东西:

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

我完全承认在这种事情上我很迷茫 所以我对目标.NET 4.5和 但是允许我的类型实现 ISerializable,并且仍然在 像.NET Fiddle 这样的环境?

(While I'm targeting .NET 4.5, I believe it's the .NET 4.0 security policy changes that caused the issue, hence the tag.)

10512 次浏览

根据 MSDN的资料,请参阅:

如何修正违规行为?

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

下面的 例子通过提供可覆盖的 ISerialable 实现修复了前两个违规行为。GetObjectData,并提供 ISerialalize 的实现。Library 类上的 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);
}
}
}

根据 MSDN,在。NET 4.0基本上不应该对部分受信任的代码使用 ISerializable,而应该使用 序列化数据

引自 https://learn.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

很重要

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

So probably not what you wanted to hear if you need it, but I don't think there's any way around it while keeping using ISerializable (other than going back to Level1 security, which you said you don't want to).

PS: ISafeSerializationData文档声明它只是为了例外情况,但它似乎并不那么具体,你可能想试一试... 我基本上不能用你的样本代码测试它(除了删除 ISerializable工作,但你已经知道了) ... 你将不得不看看 ISafeSerializationData是否适合你足够。

PS2: SecurityCritical属性无法工作,因为在部分信任模式(二级安保系统)下加载程序集时会忽略它。您可以在示例代码中看到它,如果在调用之前调试 ExecuteUntrustedCode中的 target变量,即使使用 SecurityCritical属性标记该方法,它也会从 IsSecurityTransparenttrue,从 IsSecurityCriticalfalse)

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

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

TL; DR : 原来 如果在使用者项目中使用所使用的库的内部类型,那么部分受信任的代码将按预期工作: 它能够实例化 ISerializable实现(以及一个安全关键代码不能直接调用,请参见下文)。或者,更荒谬的是,如果第一次没有成功,你可以尝试再次创建沙盒..。

But let's see some code.

类库 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]

Note: The attribute can be applied anywhere. In my case it was on a method in a random test class took me a couple of days to find.

注意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: ISerialable 实现

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

  • 应用 InternalTypeReferenceAttribute
  • sandbox is tried to be created multiple times (see the code)
  • 或者,如果所有的测试用例同时执行,并且这不是第一个

否则,在实例化 SerializableCriticalClass时会出现完全不适当的 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);
}


}

Case 2: Regular class with security critical members

The test passes under the same conditions as the first one. However, the issue is completely different here: a partially trusted code may access a security critical member directly.

[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);
}

Epilogue:

当然,这不会解决你的问题。NET Fiddle.但是现在,如果框架中没有 bug,我会非常惊讶。

现在对我来说最大的问题是被接受的答案中被引用的部分。他们是怎么编出这些废话的?ISafeSerializationData显然不是任何问题的解决方案: 它只被基类 Exception使用,如果您订阅了 SerializeObjectState事件(为什么这不是一个可重写的方法?),那么这个状态最终也会被 Exception.GetObjectData消耗掉。

AllowPartiallyTrustedCallers/SecurityCritical/SecuritySafeCritical三个属性的设计正是为了上面所示的用法。对我来说,部分受信任的代码甚至不能实例化一个类型,而不管是否尝试使用其安全关键成员,这完全是无稽之谈。但是,部分受信任的代码可以直接访问安全关键方法(参见 case 2) ,而对于透明方法,即使是来自完全受信任的域,这也是禁止的,这是一个更大的无稽之谈(实际上是 安全漏洞)。

因此,如果您的使用者项目是一个测试或另一个众所周知的程序集,那么可以完美地使用内部技巧。为了。NET Fiddle 和其他现实生活中的沙盒环境,唯一的解决方案是恢复到 SecurityRuleSet.Level1,直到这是由微软修复。


更新: 已经为该问题创建了 开发者社区通行证