背景: 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で再現する手順:
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);
}
}
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記事( first 、 second )で推奨されていますが、興味深いことに、明示的/暗黙的なインターフェイスの実装に関して異なることを行いますSecurityCritical
はNoda Timeが現在持っているものであり、 この質問の答え が示唆するものですSecuritySafeCritical
は、コード分析ルールメッセージによって多少示唆されていますSecurityPermission
またはSecurityCritical
のいずれかが存在する場合、ルールは、属性-doにAllowPartiallyTrustedCallers
がある場合を除きます。どちらの場合でも提案に従うことは役に立ちません。AllowPartiallyTrustedCallers
が適用されています。この例は、属性が適用されているかどうかにかかわらず機能しません。[Assembly: SecurityRules(SecurityRuleSet.Level1)]
をUntrustedCode
アセンブリに追加する(およびAllowPartiallyTrustedCallers
属性のコメントを外す)場合、コードは例外なく実行されますが、他のコードを妨げる可能性のある問題に対する貧弱なソリューションだと思います。
.NETのこの種のセキュリティ面に関しては、かなり失われていることを完全に認めています。だから何をcan.NET 4.5をターゲットにして、タイプでISerializable
を実装し、.NETなどの環境でも使用できるようにするフィドル?
(.NET 4.5をターゲットにしていますが、問題を引き起こしたのは.NET 4.0セキュリティポリシーの変更であり、したがってタグであると考えています。)
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)でロードされると無視されるため、機能しません。サンプルコードで確認できます。target
のExecuteUntrustedCode
変数を呼び出す直前にデバッグすると、IsSecurityTransparent
をtrue
に、IsSecurityCritical
をfalse
に設定した場合でも、SecurityCritical
属性を使用してデバッグできます)
受け入れられた答えは非常に説得力があるため、これはバグではないとほとんど信じていました。しかし、いくつかの実験を行った後、レベル2のセキュリティは完全に混乱していると言えます。少なくとも、何かが本当に怪しいです。
数日前、私はライブラリで同じ問題にぶつかりました。私はすぐに単体テストを作成しました。ただし、.NET Fiddleで経験した問題を再現できませんでしたが、まったく同じコードがコンソールアプリで例外を「成功裏に」スローしました。最終的に、この問題を克服するための2つの奇妙な方法を見つけました。
TL; DR:消費者プロジェクトで使用されるライブラリの内部タイプを使用する場合、部分的に信頼されたコードは期待どおりに動作します。ISerializable
実装をインスタンス化できます(およびセキュリティクリティカルなコードを直接呼び出すことはできませんが、以下を参照)。または、それはさらにばかげていますが、サンドボックスが初めて機能しなかった場合は、もう一度作成することができます...
しかし、いくつかのコードを見てみましょう。
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
属性にキーを適用して、テストプロジェクトの準備をします。
内部トリックを使用するには、テストアセンブリにも署名する必要があります。アセンブリ属性:
// 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;
}
}
そして、テストケースを一つ一つ見てみましょう
質問と同じ問題。テストは次の場合に合格します
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);
}
}
テストは最初のテストと同じ条件で合格します。ただし、ここでの問題はまったく異なります:部分的に信頼されたコードは、セキュリティが重要なメンバーに直接アクセスする可能性があります 。
[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());
}
}
完全を期すために、完全に信頼されたドメインで実行された上記のケースと同じケースがあります。 [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
に戻すことです。
更新:この問題に対して 開発者コミュニティチケット が作成されました。
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);
}
}
}