.net serialization .net-4.0 code-access-security

¿Cómo puedo implementar ISerializable en.NET 4+ sin violar las reglas de seguridad de herencia?



serialization .net-4.0 (3)

De acuerdo con second , en .NET 4.0 básicamente no debe usar ISerializable para código parcialmente confiable, y en su lugar debe usar ISafeSerializationData

Citando de second

Importante

En versiones anteriores a .NET Framework 4.0, la serialización de datos de usuario personalizados en un ensamblado de confianza parcial se logró mediante GetObjectData. A partir de la versión 4.0, ese método se marca con el atributo SecurityCriticalAttribute que impide la ejecución en ensamblados de confianza parcial. Para evitar esta condición, implemente la interfaz ISafeSerializationData.

Entonces, probablemente no sea lo que querías escuchar si lo necesitas, pero no creo que haya ninguna forma de ISerializable mientras ISerializable usando ISerializable (aparte de volver al nivel 1 de seguridad, que dijiste que no quieres).

PD: los documentos ISafeSerializationData indican que es solo para excepciones, pero no parece tan específico, es posible que desee ISerializable ... Básicamente no puedo probarlo con su código de muestra (aparte de eliminar ISerializable funciona, pero eso ya lo sabías) ... tendrás que ver si ISafeSerializationData te conviene lo suficiente.

PS2: el atributo SecurityCritical no funciona porque se ignora cuando el ensamblado se carga en modo de confianza parcial ( en seguridad de Nivel 2 ). Puede verlo en su código de muestra, si depura la variable de target en ExecuteUntrustedCode justo antes de invocarla, tendrá IsSecurityTransparent a true e IsSecurityCritical a false incluso si marca el método con el atributo SecurityCritical )

Antecedentes: Noda Time contiene muchas estructuras serializables. Si bien no me gusta la serialización binaria, recibimos muchas solicitudes para admitirla, en la línea de tiempo 1.x. Lo apoyamos implementando la interfaz ISerializable .

Hemos recibido un informe reciente sobre el error de Noda Time 2.x en .NET Fiddle . El mismo código que usa Noda Time 1.x funciona bien. La excepción lanzada es esta:

Reglas de seguridad de herencia violadas al anular miembro: ''NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)''. La accesibilidad de seguridad del método de anulación debe coincidir con la accesibilidad de seguridad del método que se anula.

Lo he reducido al marco que está dirigido: 1.x objetivos .NET 3.5 (perfil del cliente); 2.x objetivos .NET 4.5. Tienen grandes diferencias en términos de soporte PCL vs .NET Core y la estructura del archivo del proyecto, pero parece que esto es irrelevante.

He logrado reproducir esto en un proyecto local, pero no he encontrado una solución.

Pasos para reproducir en VS2017:

  • Crea una nueva solución
  • Cree una nueva aplicación de consola clásica de Windows dirigida a .NET 4.5.1. Lo llamé "CodeRunner".
  • En las propiedades del proyecto, vaya a Firma y firme el ensamblaje con una nueva clave. Desactive el requisito de contraseña y use cualquier nombre de archivo de clave.
  • Pegue el siguiente código para reemplazar Program.cs . Esta es una versión abreviada del código en este ejemplo de Microsoft . He mantenido todas las rutas de la misma manera, por lo que si desea volver al código más completo, no debería necesitar cambiar nada más.

Código:

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

  • Cree otro proyecto llamado "UntrustedCode". Este debería ser un proyecto clásico de la Biblioteca de clases de escritorio.
  • Firma la asamblea; puede usar una nueva clave o la misma que para CodeRunner. (Esto es en parte para imitar la situación de Noda Time y en parte para mantener feliz el Análisis de Código).
  • Pegue el siguiente código en Class1.cs (sobrescribiendo lo que hay):

Código:

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(); } } }

La ejecución del proyecto CodeRunner ofrece la siguiente excepción (reformateada para facilitar la lectura):

Excepción no controlada: System.Reflection.TargetInvocationException:
El destino de una invocación ha lanzado una excepción.
--->
System.TypeLoadException:
Reglas de seguridad de herencia violadas al anular miembro:
''UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
La accesibilidad de seguridad del método de anulación debe coincidir con la seguridad
Accesibilidad del método que se anula.

Los atributos comentados muestran cosas que he intentado:

  • SecurityPermission es recomendado por dos artículos diferentes de MS ( first , second ), aunque curiosamente hacen cosas diferentes en torno a la implementación de interfaz explícita / implícita
  • SecurityCritical es lo que Noda Time tiene actualmente, y es lo que sugiere la respuesta de esta pregunta
  • SecuritySafeCritical es algo sugerido por los mensajes de reglas de Code Analysis
  • Sin ningún atributo, las reglas de Code Analysis son felices, ya sea con SecurityPermission o SecurityCritical presente, las reglas le indican que elimine los atributos, a menos que tenga AllowPartiallyTrustedCallers . Seguir las sugerencias en cualquier caso no ayuda.
  • Noda Time ha aplicado AllowPartiallyTrustedCallers ; El ejemplo aquí no funciona con o sin el atributo aplicado.

El código se ejecuta sin excepción si agrego [assembly: SecurityRules(SecurityRuleSet.Level1)] al ensamblado UntrustedCode (y descomenta el atributo AllowPartiallyTrustedCallers ), pero creo que es una mala solución al problema que podría obstaculizar otro código.

Admito totalmente estar bastante perdido cuando se trata de este tipo de aspecto de seguridad de .NET. Entonces, ¿qué puedo hacer para apuntar a .NET 4.5 y, sin embargo, permitir que mis tipos implementen ISerializable y aún se utilicen en entornos como .NET Fiddle?

(Aunque estoy apuntando a .NET 4.5, creo que son los cambios en la política de seguridad de .NET 4.0 los que causaron el problema, de ahí la etiqueta).


De acuerdo con MSDN ver:

¿Cómo arreglar las violaciones?

Para corregir una violación de esta regla, haga que el método GetObjectData visible y reemplazable y asegúrese de que todos los campos de instancia estén incluidos en el proceso de serialización o marcados explícitamente con el atributo NonSerializedAttribute .

El siguiente example corrige las dos infracciones anteriores al proporcionar una implementación reemplazable de ISerializable.GetObjectData en la clase Book y al proporcionar una implementación de ISerializable.GetObjectData en la clase Library.

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


La respuesta aceptada es tan convincente que casi creí que esto no era un error. Pero después de hacer algunos experimentos ahora puedo decir que la seguridad de Level2 es un completo desastre; Al menos, algo es realmente sospechoso.

Hace un par de días me encontré con el mismo problema con mis bibliotecas. Rápidamente creé una prueba unitaria; sin embargo, no pude reproducir el problema que experimenté en .NET Fiddle, mientras que el mismo código "con éxito" arrojó la excepción en una aplicación de consola. Al final encontré dos formas extrañas de superar el problema.

TL; DR : Resulta que si usa un tipo interno de la biblioteca usada en su proyecto de consumidor, entonces el código parcialmente confiable funciona como se esperaba: puede instanciar una implementación ISerializable (y no se puede llamar directamente a un código crítico de seguridad) , pero ver más abajo). O, lo que es aún más ridículo, puede intentar crear el sandbox nuevamente si no funcionó por primera vez ...

Pero veamos algo de código.

ClassLibrary.dll:

ISerializable dos casos: uno para una clase regular con contenido crítico de seguridad y una implementación 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) { } }

Una forma de superar el problema es utilizar un tipo interno del ensamblaje del consumidor. Cualquier tipo lo hará; ahora defino un atributo:

[AttributeUsage(AttributeTargets.All)] internal class InternalTypeReferenceAttribute : Attribute { public InternalTypeReferenceAttribute() { } }

Y los atributos relevantes aplicados al ensamblaje:

[assembly: InternalsVisibleTo("UnitTest, PublicKey=<your public key>")] [assembly: AllowPartiallyTrustedCallers] [assembly: SecurityRules(SecurityRuleSet.Level2, SkipVerificationInFullTrust = true)]

Firme el ensamblaje, aplique la clave al atributo InternalsVisibleTo y prepárese para el proyecto de prueba:

UnitTest.dll (usa NUnit y ClassLibrary):

Para usar el truco interno, el conjunto de prueba también debe estar firmado. Atributos de ensamblaje:

// 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]

Nota : El atributo se puede aplicar en cualquier lugar. En mi caso, fue en un método en una clase de prueba aleatoria que me llevó un par de días encontrar.

Nota 2 : Si ejecuta todos los métodos de prueba juntos, puede suceder que las pruebas pasen.

El esqueleto de la clase de prueba:

[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; } }

Y veamos los casos de prueba uno por uno

Caso 1: implementación ISerializable

El mismo problema que en la pregunta. La prueba pasa si

  • InternalTypeReferenceAttribute se aplica
  • Se intenta crear sandbox varias veces (ver el código)
  • o, si todos los casos de prueba se ejecutan a la vez y este no es el primero

De lo contrario, vienen las Inheritance security rules violated while overriding member... totalmente inapropiadas Inheritance security rules violated while overriding member... excepción de Inheritance security rules violated while overriding member... cuando crea una instancia de 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); } }

Caso 2: clase regular con miembros críticos de seguridad

La prueba pasa bajo las mismas condiciones que la primera. Sin embargo, el problema es completamente diferente aquí: un código de confianza parcial puede acceder directamente a un miembro crítico de seguridad .

[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()); } }

Caso 3-4: versiones de confianza completa del caso 1-2

En aras de la exhaustividad, aquí están los mismos casos que los anteriores ejecutados en un dominio totalmente confiable. Si elimina [assembly: AllowPartiallyTrustedCallers] las pruebas fallan porque entonces puede acceder al código crítico directamente (ya que los métodos ya no son transparentes por defecto).

[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); }

Epílogo:

Por supuesto, esto no resolverá su problema con .NET Fiddle. Pero ahora estaría muy sorprendido si no fuera un error en el marco.

La pregunta más importante para mí ahora es la parte citada en la respuesta aceptada. ¿Cómo salieron con estas tonterías? ISafeSerializationData claramente no es una solución para nada: se usa exclusivamente por la clase de Exception base y si suscribe el evento SerializeObjectState (¿por qué no es un método reemplazable?), Entonces el estado también será consumido por Exception.GetObjectData en el final.

El triunvirato de atributos AllowPartiallyTrustedCallers / SecurityCritical / SecuritySafeCritical se diseñaron exactamente para el uso que se muestra arriba. Me parece totalmente absurdo que un código de confianza parcial ni siquiera pueda crear una instancia de un tipo, independientemente del intento de usar sus miembros críticos de seguridad. Pero es una tontería aún mayor (un agujero de seguridad en realidad) que un código parcialmente confiable pueda acceder directamente a un método crítico de seguridad (ver caso 2 ), mientras que esto está prohibido para métodos transparentes incluso desde un dominio totalmente confiable.

Entonces, si su proyecto de consumidor es una prueba u otro ensamblaje conocido, entonces el truco interno se puede usar perfectamente. Para .NET Fiddle y otros entornos de espacio aislado de la vida real, la única solución es volver a SecurityRuleSet.Level1 hasta que Microsoft lo solucione.

Actualización: se ha creado un ticket de la comunidad de desarrolladores para el problema.