.net serializable




如何在不違反繼承安全規則的情況下在.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