web-dev-qa-db-ja.com

継承セキュリティルールに違反せずに.NET 4+でISerializableを実装するにはどうすればよいですか?

背景: Noda Time には、多くのシリアル化可能な構造体が含まれています。私はバイナリシリアル化が嫌いですが、1.xのタイムラインで、それをサポートするための多くのリクエストを受け取りました。 ISerializableインターフェースを実装することでサポートしています。

Noda Time 2.xの最近の issue report を受け取りました 。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のキーと同じキーを使用できます。 (これは、一部は野田時間の状況を模倣するためであり、一部はコード分析を満足させるためです。)
  • 次のコードを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は、2つの異なるMS記事( firstsecond )で推奨されていますが、興味深いことに、明示的/暗黙的なインターフェイスの実装に関して異なることを行います
  • SecurityCriticalはNoda Timeが現在持っているものであり、 この質問の答え が示唆するものです
  • SecuritySafeCriticalは、コード分析ルールメッセージによって多少示唆されています
  • any属性がない場合、コード分析ルールは満足です-SecurityPermissionまたはSecurityCriticalのいずれかが存在する場合、ルールは、属性-doAllowPartiallyTrustedCallersがある場合を除きます。どちらの場合でも提案に従うことは役に立ちません。
  • 野田時間にはAllowPartiallyTrustedCallersが適用されています。この例は、属性が適用されているかどうかにかかわらず機能しません。

[Assembly: SecurityRules(SecurityRuleSet.Level1)]UntrustedCodeアセンブリに追加する(およびAllowPartiallyTrustedCallers属性のコメントを外す)場合、コードは例外なく実行されますが、他のコードを妨げる可能性のある問題に対する貧弱なソリューションだと思います。

.NETのこの種のセキュリティ面に関しては、かなり失われていることを完全に認めています。だから何をcan.NET 4.5をターゲットにして、タイプでISerializableを実装し、.NETなどの環境でも使用できるようにするフィドル?

(.NET 4.5をターゲットにしていますが、問題を引き起こしたのは.NET 4.0セキュリティポリシーの変更であり、したがってタグであると考えています。)

91
Jon Skeet

MSDN によると、.NET 4.0では基本的に、部分的に信頼できるコードにISerializableを使用するべきではなく、代わりに ISafeSerializationData を使用する必要があります

https://docs.Microsoft.com/en-us/dotnet/standard/serialization/custom-serialization から引用

重要

.NET Framework 4.0より前のバージョンでは、部分的に信頼されたアセンブリでのカスタムユーザーデータのシリアル化は、GetObjectDataを使用して実現されていました。バージョン4.0以降、そのメソッドはSecurityCriticalAttribute属性でマークされ、部分的に信頼されたアセンブリでの実行を防ぎます。この状態を回避するには、ISafeSerializationDataインターフェイスを実装します。

だからおそらくあなたがそれを必要とするならあなたが聞きたいことではないでしょうが、ISerializableを使い続けている間、あなたはそれを回避する方法はないと思います(Level1セキュリティに戻ることを除いて)。

PS:ISafeSerializationDataのドキュメントは、例外のためだけのものであると述べていますが、それほど具体的ではないようです。試してみたいと思うかもしれません...基本的にサンプルコードでテストすることはできません(ISerializable動作しますが、あなたはすでにそれを知っていました)... ISafeSerializationDataがあなたに十分であるかどうか見る必要があります。

PS2:SecurityCritical属性は、Assemblyが部分信頼モード(on Level2 security)でロードされると無視されるため、機能しません。サンプルコードで確認できます。targetExecuteUntrustedCode変数を呼び出す直前にデバッグすると、IsSecurityTransparenttrueに、IsSecurityCriticalfalseに設定した場合でも、SecurityCritical属性を使用してデバッグできます)

46
Jcl

受け入れられた答えは非常に説得力があるため、これはバグではないとほとんど信じていました。しかし、いくつかの実験を行った後、レベル2のセキュリティは完全に混乱していると言えます。少なくとも、何かが本当に怪しいです。

数日前、私はライブラリで同じ問題にぶつかりました。私はすぐに単体テストを作成しました。ただし、.NET Fiddleで経験した問題を再現できませんでしたが、まったく同じコードがコンソールアプリで例外を「成功裏に」スローしました。最終的に、この問題を克服するための2つの奇妙な方法を見つけました。

TL; DR:消費者プロジェクトで使用されるライブラリの内部タイプを使用する場合、部分的に信頼されたコードは期待どおりに動作します。ISerializable実装をインスタンス化できます(およびセキュリティクリティカルなコードを直接呼び出すことはできませんが、以下を参照)。または、それはさらにばかげていますが、サンドボックスが初めて機能しなかった場合は、もう一度作成することができます...

しかし、いくつかのコードを見てみましょう。

ClassLibrary.dll:

2つのケースを分けましょう。1つはセキュリティが重要なコンテンツを含む通常のクラス、もう1つは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) { }
}

この問題を解決する1つの方法は、消費者会議の内部タイプを使用することです。どのタイプでも実行できます。今、私は属性を定義します:

[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実装

質問と同じ問題。テストは次の場合に合格します

  • InternalTypeReferenceAttributeが適用されます
  • サンドボックスは複数回作成されます(コードを参照)
  • または、すべてのテストケースが一度に実行され、これが最初のテストケースではない場合

そうしないと、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:セキュリティが重要なメンバーを含む通常のクラス

テストは最初のテストと同じ条件で合格します。ただし、ここでの問題はまったく異なります:部分的に信頼されたコードは、セキュリティが重要なメンバーに直接アクセスする可能性があります

[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の3種類の属性は、上記の使用方法に合わせて設計されています。部分的に信頼されたコードは、セキュリティが重要なメンバーを使用する試みに関係なく、型をインスタンス化することさえできないというのは、私にはまったく無意味に思えます。しかし、部分的に信頼されたコードがセキュリティクリティカルなメソッドに直接アクセスすることは、さらに大きなナンセンス(セキュリティホール)です(ケース2を参照) )一方、これは完全に信頼されたドメインからの透過的な方法でも禁止されています。

したがって、コンシューマプロジェクトがテストまたは別の有名なアセンブリである場合、内部のトリックを完全に使用できます。 .NET Fiddleおよびその他の実際のサンドボックス環境では、Microsoftによって修正されるまで、唯一の解決策はSecurityRuleSet.Level1に戻すことです。


更新:この問題に対して 開発者コミュニティチケット が作成されました。

3
György Kőszeg

MSDN によると:

違反の修正方法は?

このルールの違反を修正するには、 GetObjectData メソッドを表示およびオーバーライド可能にし、すべてのインスタンスフィールドがシリアル化プロセスに含まれるか、または NonSerializedAttribute 属性で明示的にマークされていることを確認します。

次の example は、BookクラスでISerializable.GetObjectDataのオーバーライド可能な実装を提供し、LibraryクラスでISerializable.GetObjectDataの実装を提供することにより、以前の2つの違反を修正します。

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);
        }
    }
}
2
5377037