公认的答案是如此令人信服,以至于我几乎相信这不是一个错误。但是,经过一些实验之后,我可以说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修复的解决方案。
更新:一个开发者社区车票已经为这个问题产生。
AllowPartiallyTrustedCallers
,但这似乎没有什么不同