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


109

背景: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と同じキーを使用できます。(これは、一部は野田時間の状況を模倣するためであり、一部はコード分析を幸せに保つためです。)
  • 次のコードを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(...)。
オーバーライドするメソッドのセキュリティアクセシビリティは、オーバーライドされるメソッドのセキュリティ
アクセシビリティと一致する必要があります。

コメントアウトされた属性は、私が試したことを示しています。

  • SecurityPermission2つの異なるMS記事(firstsecond)で推奨されていますが、興味深いことに、明示的/暗黙的なインターフェース実装に関して異なることをしています。
  • SecurityCritical野田タイムが現在持っているものであり、この質問の答えが示唆するものです
  • SecuritySafeCritical コード分​​析ルールのメッセージでやや示唆されている
  • せずに任意の属性は、コード分析ルールは満足している-のいずれかでSecurityPermission、またはSecurityCritical あなたがない限り-現在、ルールは、属性を削除するためにあなたを教えてください持っていますAllowPartiallyTrustedCallers。どちらの場合も提案に従うことは役に立ちません。
  • 野田タイムがそれにAllowPartiallyTrustedCallers適用されました。この例は、属性を適用してもしなくても機能しません。

アセンブリに追加[assembly: SecurityRules(SecurityRuleSet.Level1)]した場合UntrustedCode(およびAllowPartiallyTrustedCallers属性のコメントを解除した場合)、コードは例外なく実行されますが、他のコードを妨げる可能性がある問題の解決策としては不十分だと思います。

.NETのこの種のセキュリティの側面に関しては、私は完全に迷っていることを完全に認めています。では、.NET 4.5をターゲットにして、タイプを実装し、.NET Fiddleなどの環境で引き続き使用できるようにするにはどうISerializableすればよいですか?

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


興味深いことに、4.0でのセキュリティモデルの変更に関するこの説明は、単純に削除するだけAllowPartiallyTrustedCallersでうまくいくことを示唆していますが、違いはないようです
Mathias R. Jessen

回答:


56

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インターフェイスを実装します。

おそらく、あなたがそれを必要とする場合に聞きたかったことはおそらくないでしょうが、私は使い続けている間はそれを回避する方法はないと思いますISerializableLevel1セキュリティに戻って、あなたがしたくないと言った以外は)。

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

PS2:SecurityCriticalアセンブリが部分信頼モード(レベル2セキュリティ)で読み込まれると無視されるため、属性は機能しません。あなたのサンプルコードにそれを見ることができますが、デバッグする場合は、target変数をExecuteUntrustedCode、それを呼び出す前に右、それはあるでしょうIsSecurityTransparenttrueIsSecurityCriticalfalseあなたが持つメソッドをマークしてもSecurityCritical)属性


Aha-説明してくれてありがとう。ここで例外が恥ずかしいです。何をすべきかを考える必要があります...
Jon Skeet

@JonSkeet正直に言って、バイナリシリアライゼーションはすべて破棄します...しかし、ユーザーベースが気に入らない可能性があることを理解しています
Jcl

私はそれをしなければならないだろうと思います-これはv3.0に移行することを意味します。他にもメリットがあります...野田タイムのコミュニティに相談する必要があります。
Jon Skeet

12
@JonSkeetところで、興味があれば、この記事ではレベル1とレベル2のセキュリティの違いについて説明します(なぜ機能しないのか)
Jcl

8

受け入れられた答えは非常に説得力があるので、これはバグではないとほぼ信じていました。しかし、いくつかの実験を行った後、レベル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 適用されます
  • サンドボックスが複数回作成されようとしている(コードを参照)
  • または、すべてのテストケースが一度に実行され、これが最初のものではない場合

それ以外の場合は、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によって修正されるまで、唯一のソリューションは元に戻ります。


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


2

MSDNによると、参照してください:

違反を修正する方法?

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

次のでは、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
リンクした記事はCA2240向けであり、起動されていません-コードはそれに違反していません。これは構造体なので、効果的に密封されます。フィールドはありません。GetObjectData明示的に実装しますが、暗黙的に実装しても役に立ちません。
Jon Skeet

15
承知しました。お試しいただきありがとうございます。ただし、機能しない理由を説明します。(そして推奨事項として、このようなトリッキーなものについては、質問に検証可能な例が含まれているため、提案された修正を適用して実際に役立つかどうかを確認することをお勧めします。)
Jon Skeet
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.