如何在不违反继承安全规则的情况下在 .NET 4+ 中实现 ISerializable?
How can I implement ISerializable in .NET 4+ without violating inheritance security rules?
背景:Noda Time包含很多
可序列化结构。虽然我不喜欢二进制序列化,但我们
在 1.x 时间线中收到了许多支持它的请求。
我们通过实现 ISerializable
接口来支持它。
我们最近收到了一个 问题
野田的报告
时间 2.x 在 .NET 中失败
Fiddle。使用 Noda 的相同代码
时间 1.x 工作正常。抛出的异常是这样的:
Inheritance security rules violated while overriding member:
'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo,
System.Runtime.Serialization.StreamingContext)'. Security
accessibility of the overriding method must match the security
accessibility of the method being overriden.
我已将其缩小到目标框架:1.x
目标 .NET 3.5(客户端配置文件); 2.x 以 .NET 4.5 为目标。他们有
PCL 与 .NET Core 和
项目文件结构,但看起来这是无关紧要的。
我已经在本地项目中重现了这个,但我没有
找到了解决办法。
在 VS2017 中重现的步骤:
- 创建新解决方案
- 创建新的经典 Windows 控制台应用程序,以 .NET 为目标
4.5.1.我叫它 "CodeRunner".
- 在项目属性中,转到签名并使用
一把新钥匙。取消密码要求,并使用任何密钥文件名。
- 粘贴以下代码以替换
Program.cs
。这是个
this 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" 的项目。这应该是一个
Classic Desktop Class 库项目。
- 签署大会;您可以使用新密钥或与
代码运行器。 (这部分是为了模仿野田时代的情况,
部分原因是为了让代码分析开心。)
- 将以下代码粘贴到
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 项目给出了以下异常(为了便于阅读而重新格式化):
Unhandled Exception: System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation.
--->
System.TypeLoadException:
Inheritance security rules violated while overriding member:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
Security accessibility of the overriding method must match the security
accessibility of the method being overriden.
注释掉的属性显示了我尝试过的东西:
SecurityPermission
由两篇不同的 MS 文章推荐 (first,
second), 虽然
有趣的是,他们围绕 explicit/implicit 接口实现 做了不同的事情
SecurityCritical
是野田时代目前拥有的,也是this question's answer建议的
SecuritySafeCritical
有点像代码分析规则消息
- 没有 any 属性,代码分析规则很满意 -
SecurityPermission
或 SecurityCritical
目前,规则告诉您删除属性 - 除非您 do 有 AllowPartiallyTrustedCallers
。在任何一种情况下遵循建议都无济于事。
- Noda Time 应用了
AllowPartiallyTrustedCallers
;此处的示例在应用或不应用属性的情况下都不起作用。
如果我将 [assembly: SecurityRules(SecurityRuleSet.Level1)]
添加到 UntrustedCode
程序集(并取消注释 AllowPartiallyTrustedCallers
属性),代码运行时不会出现异常,但我认为这是解决问题的糟糕解决方案可能会妨碍其他代码。
我完全承认在谈到这种事情时我很迷茫
.NET 的安全方面。那么我可以做什么来瞄准.NET 4.5和
但允许我的类型实现 ISerializable
并且仍然用于
.NET 等环境 Fiddle?
(虽然我的目标是 .NET 4.5,但我认为是 .NET 4.0 安全策略更改导致了问题,因此是标记。)
根据MSDN见:
How to Fix Violations?
To fix a violation of this rule, make the GetObjectData method visible and overridable and make sure all instance fields are included in the serialization process or explicitly marked with the NonSerializedAttribute attribute.
The following example fixes the two previous violations by providing an overrideable implementation of ISerializable.GetObjectData on the Book class and by providing an implementation of ISerializable.GetObjectData on the Library class.
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);
}
}
}
根据the MSDN, in .NET 4.0 basically you should not use ISerializable
for partially trusted code, and instead you should use ISafeSerializationData
引用自https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization
Important
In versions previous to .NET Framework 4.0, serialization of custom user data in a partially trusted assembly was accomplished using the GetObjectData. Starting with version 4.0, that method is marked with the SecurityCriticalAttribute attribute which prevents execution in partially trusted assemblies. To work around this condition, implement the ISafeSerializationData interface.
所以如果你需要它可能不是你想听到的,但我认为在继续使用 ISerializable
的同时没有任何解决方法(除了回到 Level1
安全,你说你不想的)。
PS: ISafeSerializationData
文档说它只是为了例外,但它似乎并没有那么具体,你可能想试一试......我基本上可以用您的示例代码测试它(除了删除 ISerializable
之外,但您已经知道)...您必须看看 ISafeSerializationData
是否足够适合您。
PS2:SecurityCritical
属性不起作用,因为在部分信任模式下加载程序集时会忽略它(on Level2 安全性) .你可以在你的示例代码中看到它,如果你在调用它之前调试 ExecuteUntrustedCode
中的 target
变量,它会有 IsSecurityTransparent
到 true
和 IsSecurityCritical
到 false
,即使您使用 SecurityCritical
属性标记该方法)
接受的答案是如此令人信服,以至于我几乎相信这不是一个错误。但是现在做了一些实验之后,我可以说 Level2 安全性完全是一团糟;至少,有些东西真的很可疑。
几天前,我在我的库中遇到了同样的问题。我很快创建了一个单元测试;但是,我无法重现我在 .NET Fiddle 中遇到的问题,而完全相同的代码“成功地”在控制台应用程序中引发了异常。最后我找到了两个奇怪的方法来解决这个问题。
TL;DR:事实证明,如果您在您的消费者项目中使用所用库的内部类型,那么部分受信任的代码将作为预期:它能够实例化一个 ISerializable
实现 (并且不能直接调用安全关键代码,但请参见下文)。或者,更可笑的是,如果第一次没有成功,您可以尝试重新创建沙箱...
但是让我们看一些代码。
ClassLibrary.dll:
让我们分开两种情况:一种是针对具有安全关键内容的常规 class,另一种是 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]
注意:该属性可以应用于任何地方。在我的例子中,它是在随机测试中的一个方法 class 花了我几天时间才找到的。
注意 2:如果您 运行 将所有测试方法放在一起,测试可能会通过。
测试骨架class:
[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
已应用
- 多次尝试创建sandbox(见代码)
- 或者,如果一次执行所有测试用例并且这不是第一个
否则,实例化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);
}
}
案例 2:常规 class 具有安全关键成员
测试在与第一个相同的条件下通过。但是,这里的问题完全不同:部分受信任的代码可能会直接访问安全关键成员。
[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
class 使用,如果您订阅 SerializeObjectState
事件(为什么不是可覆盖的method?),那么状态最后也会被Exception.GetObjectData
消耗掉。
AllowPartiallyTrustedCallers
/SecurityCritical
/SecuritySafeCritical
属性的三巨头正是为上面显示的用法而设计的。对我来说,部分受信任的代码甚至无法实例化类型,无论是否尝试使用其安全关键成员,这似乎完全是胡说八道。但这是一个更大的废话(实际上是 安全漏洞 )部分受信任的代码可以直接访问安全关键方法(参见 案例 2)而即使来自完全受信任的域,透明方法也是被禁止的。
因此,如果您的消费者项目是一个测试或另一个 well-known 程序集,那么可以完美地使用内部技巧。对于 .NET Fiddle 和其他 real-life 沙盒环境,唯一的解决方案是恢复到 SecurityRuleSet.Level1
,直到 Microsoft 修复此问题。
更新: 已为问题创建 Developer Community ticket。
背景:Noda Time包含很多
可序列化结构。虽然我不喜欢二进制序列化,但我们
在 1.x 时间线中收到了许多支持它的请求。
我们通过实现 ISerializable
接口来支持它。
我们最近收到了一个 问题 野田的报告 时间 2.x 在 .NET 中失败 Fiddle。使用 Noda 的相同代码 时间 1.x 工作正常。抛出的异常是这样的:
Inheritance security rules violated while overriding member: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. Security accessibility of the overriding method must match the security accessibility of the method being overriden.
我已将其缩小到目标框架:1.x 目标 .NET 3.5(客户端配置文件); 2.x 以 .NET 4.5 为目标。他们有 PCL 与 .NET Core 和 项目文件结构,但看起来这是无关紧要的。
我已经在本地项目中重现了这个,但我没有 找到了解决办法。
在 VS2017 中重现的步骤:
- 创建新解决方案
- 创建新的经典 Windows 控制台应用程序,以 .NET 为目标 4.5.1.我叫它 "CodeRunner".
- 在项目属性中,转到签名并使用 一把新钥匙。取消密码要求,并使用任何密钥文件名。
- 粘贴以下代码以替换
Program.cs
。这是个 this 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" 的项目。这应该是一个 Classic Desktop Class 库项目。
- 签署大会;您可以使用新密钥或与 代码运行器。 (这部分是为了模仿野田时代的情况, 部分原因是为了让代码分析开心。)
- 将以下代码粘贴到
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 项目给出了以下异常(为了便于阅读而重新格式化):
Unhandled Exception: System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation.
--->
System.TypeLoadException:
Inheritance security rules violated while overriding member:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
Security accessibility of the overriding method must match the security
accessibility of the method being overriden.
注释掉的属性显示了我尝试过的东西:
SecurityPermission
由两篇不同的 MS 文章推荐 (first, second), 虽然 有趣的是,他们围绕 explicit/implicit 接口实现 做了不同的事情
SecurityCritical
是野田时代目前拥有的,也是this question's answer建议的SecuritySafeCritical
有点像代码分析规则消息- 没有 any 属性,代码分析规则很满意 -
SecurityPermission
或SecurityCritical
目前,规则告诉您删除属性 - 除非您 do 有AllowPartiallyTrustedCallers
。在任何一种情况下遵循建议都无济于事。 - Noda Time 应用了
AllowPartiallyTrustedCallers
;此处的示例在应用或不应用属性的情况下都不起作用。
如果我将 [assembly: SecurityRules(SecurityRuleSet.Level1)]
添加到 UntrustedCode
程序集(并取消注释 AllowPartiallyTrustedCallers
属性),代码运行时不会出现异常,但我认为这是解决问题的糟糕解决方案可能会妨碍其他代码。
我完全承认在谈到这种事情时我很迷茫
.NET 的安全方面。那么我可以做什么来瞄准.NET 4.5和
但允许我的类型实现 ISerializable
并且仍然用于
.NET 等环境 Fiddle?
(虽然我的目标是 .NET 4.5,但我认为是 .NET 4.0 安全策略更改导致了问题,因此是标记。)
根据MSDN见:
How to Fix Violations?
To fix a violation of this rule, make the GetObjectData method visible and overridable and make sure all instance fields are included in the serialization process or explicitly marked with the NonSerializedAttribute attribute.
The following example fixes the two previous violations by providing an overrideable implementation of ISerializable.GetObjectData on the Book class and by providing an implementation of ISerializable.GetObjectData on the Library class.
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);
}
}
}
根据the MSDN, in .NET 4.0 basically you should not use ISerializable
for partially trusted code, and instead you should use ISafeSerializationData
引用自https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization
Important
In versions previous to .NET Framework 4.0, serialization of custom user data in a partially trusted assembly was accomplished using the GetObjectData. Starting with version 4.0, that method is marked with the SecurityCriticalAttribute attribute which prevents execution in partially trusted assemblies. To work around this condition, implement the ISafeSerializationData interface.
所以如果你需要它可能不是你想听到的,但我认为在继续使用 ISerializable
的同时没有任何解决方法(除了回到 Level1
安全,你说你不想的)。
PS: ISafeSerializationData
文档说它只是为了例外,但它似乎并没有那么具体,你可能想试一试......我基本上可以用您的示例代码测试它(除了删除 ISerializable
之外,但您已经知道)...您必须看看 ISafeSerializationData
是否足够适合您。
PS2:SecurityCritical
属性不起作用,因为在部分信任模式下加载程序集时会忽略它(on Level2 安全性) .你可以在你的示例代码中看到它,如果你在调用它之前调试 ExecuteUntrustedCode
中的 target
变量,它会有 IsSecurityTransparent
到 true
和 IsSecurityCritical
到 false
,即使您使用 SecurityCritical
属性标记该方法)
接受的答案是如此令人信服,以至于我几乎相信这不是一个错误。但是现在做了一些实验之后,我可以说 Level2 安全性完全是一团糟;至少,有些东西真的很可疑。
几天前,我在我的库中遇到了同样的问题。我很快创建了一个单元测试;但是,我无法重现我在 .NET Fiddle 中遇到的问题,而完全相同的代码“成功地”在控制台应用程序中引发了异常。最后我找到了两个奇怪的方法来解决这个问题。
TL;DR:事实证明,如果您在您的消费者项目中使用所用库的内部类型,那么部分受信任的代码将作为预期:它能够实例化一个 ISerializable
实现 (并且不能直接调用安全关键代码,但请参见下文)。或者,更可笑的是,如果第一次没有成功,您可以尝试重新创建沙箱...
但是让我们看一些代码。
ClassLibrary.dll:
让我们分开两种情况:一种是针对具有安全关键内容的常规 class,另一种是 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]
注意:该属性可以应用于任何地方。在我的例子中,它是在随机测试中的一个方法 class 花了我几天时间才找到的。
注意 2:如果您 运行 将所有测试方法放在一起,测试可能会通过。
测试骨架class:
[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
已应用- 多次尝试创建sandbox(见代码)
- 或者,如果一次执行所有测试用例并且这不是第一个
否则,实例化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);
}
}
案例 2:常规 class 具有安全关键成员
测试在与第一个相同的条件下通过。但是,这里的问题完全不同:部分受信任的代码可能会直接访问安全关键成员。
[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
class 使用,如果您订阅 SerializeObjectState
事件(为什么不是可覆盖的method?),那么状态最后也会被Exception.GetObjectData
消耗掉。
AllowPartiallyTrustedCallers
/SecurityCritical
/SecuritySafeCritical
属性的三巨头正是为上面显示的用法而设计的。对我来说,部分受信任的代码甚至无法实例化类型,无论是否尝试使用其安全关键成员,这似乎完全是胡说八道。但这是一个更大的废话(实际上是 安全漏洞 )部分受信任的代码可以直接访问安全关键方法(参见 案例 2)而即使来自完全受信任的域,透明方法也是被禁止的。
因此,如果您的消费者项目是一个测试或另一个 well-known 程序集,那么可以完美地使用内部技巧。对于 .NET Fiddle 和其他 real-life 沙盒环境,唯一的解决方案是恢复到 SecurityRuleSet.Level1
,直到 Microsoft 修复此问题。
更新: 已为问题创建 Developer Community ticket。