배경 : Noda Time 에는 직렬화 가능한 구조체가 많이 포함되어 있습니다. 이진 직렬화를 싫어하지만 1.x 타임 라인에서 지원해 달라는 많은 요청을 받았습니다. ISerializable
인터페이스 를 구현하여 지원합니다 .
.NET Fiddle 내에서 실패 하는 Noda Time 2.x의 최근 문제 보고서 를 받았습니다 . 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 상황을 모방하고 부분적으로 코드 분석을 행복하게 유지하기위한 것입니다.)
- 다음 코드를 붙여 넣으십시오
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 :
호출 대상에서 예외가 throw되었습니다.
—>
System.TypeLoadException :
멤버를 재정의하는 동안 상속 보안 규칙 위반 :
‘UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (…).
재정의하는 메서드의 보안 접근성은 재정의되는 메서드의 보안
접근성 과 일치해야합니다 .
주석 처리 된 속성은 내가 시도한 것을 보여줍니다.
SecurityPermission
흥미롭게도 명시 적 / 암시 적 인터페이스 구현에 대해 서로 다른 작업을 수행하지만 두 개의 서로 다른 MS 기사 ( 첫 번째 ,
두 번째 )에서 권장합니다.SecurityCritical
Noda Time이 현재 가지고있는 것이며, 이 질문의 답변이 제안하는 것입니다.SecuritySafeCritical
코드 분석 규칙 메시지에서 다소 제안됩니다.- 없이 어떤 속성 코드 분석 규칙은 행복하다 – 하나와 함께
SecurityPermission
또는SecurityCritical
현재, 규칙 속성을 제거하는 당신에게 – 당신이하지 않으면 할 수 있습니다AllowPartiallyTrustedCallers
. 두 경우 모두 제안 사항을 따르는 것은 도움이되지 않습니다. - Noda Time이
AllowPartiallyTrustedCallers
적용되었습니다. 여기의 예는 속성이 적용되거나 적용되지 않고 작동하지 않습니다.
예외없이 코드가 실행 내가 추가 할 경우 [assembly: SecurityRules(SecurityRuleSet.Level1)]
받는 UntrustedCode
조립 (과의 주석 AllowPartiallyTrustedCallers
속성),하지만 난 그 다른 코드를 방해 할 수있는 문제에 대한 빈약 한 솔루션입니다 생각합니다.
.NET의 이런 종류의 보안 측면에 관해서는 꽤 잃어버린 것을 완전히 인정합니다. 그래서 수 있습니다 나는 .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
보안 으로 돌아가고 싶지 않다고 말한 것 외에는 ).
추신 : ISafeSerializationData
문서에 예외 용이라고 명시되어 있지만 그다지 구체적으로 보이지는 않습니다. 한 번 시도해 볼 수 있습니다 … 기본적으로 샘플 코드로 테스트 할 수 없습니다 ( ISerializable
작업을 제거하는 것 외에는 하지만 당신은 이미 알고있었습니다) … 당신은 ISafeSerializationData
당신에게 충분히 적합한 지 확인해야 할 것입니다.
PS2 : SecurityCritical
어셈블리가 부분 신뢰 모드 ( 레벨 2 보안 ) 에서로드 될 때 무시되기 때문에 특성이 작동하지 않습니다 . 당신은 샘플 코드에서 볼 수있는 디버그 경우 target
의 변수 ExecuteUntrustedCode
를 호출하기 전에 권리를이해야 IsSecurityTransparent
에 true
와 IsSecurityCritical
에 false
당신이와 방법으로 표시하는 경우에도 SecurityCritical
) 속성을
답변
받아 들여진 대답은 너무 설득력이있어서 이것이 버그가 아니라고 거의 믿었습니다. 그러나 몇 가지 실험을 한 후에는 Level2 보안이 완전히 엉망이라고 말할 수 있습니다. 적어도 뭔가 정말 비린내입니다.
며칠 전에 도서관에서 같은 문제에 부딪 혔습니다. 빠르게 단위 테스트를 만들었습니다. 그러나 .NET Fiddle에서 경험 한 문제를 재현 할 수 없었고, 동일한 코드가 “성공적으로”콘솔 앱에서 예외를 던졌습니다. 결국 나는 문제를 극복하는 두 가지 이상한 방법을 발견했습니다.
요약 : 소비자 프로젝트에서 사용 된 라이브러리의 내부 유형을 사용하면 부분적으로 신뢰할 수있는 코드가 예상대로 작동합니다. 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 구현
질문에서와 같은 문제입니다. 테스트는 다음과 같은 경우 통과합니다.
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에서이 문제 를 해결할 때까지 되 돌리는 것 입니다.
업데이트 : 문제에 대한 개발자 커뮤니티 티켓 이 생성되었습니다.
답변
MSDN 에 따르면 다음을 참조하십시오.
위반 사항을 수정하는 방법?
이 규칙 위반을 수정하려면 GetObjectData 메서드를 표시하고 재정의 할 수 있도록 만들고 모든 인스턴스 필드가 serialization 프로세스에 포함되어 있는지 또는 NonSerializedAttribute 특성 으로 명시 적으로 표시되었는지 확인 합니다.
다음 예제 에서는 Book 클래스에 ISerializable.GetObjectData의 재정의 가능한 구현을 제공하고 Library 클래스에 ISerializable.GetObjectData의 구현을 제공하여 이전의 두 가지 위반을 수정합니다.
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);
}
}
}