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

109

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 ISerializableinterfaz.

Recibimos un informe de un problema reciente de Noda Time 2.x fallando dentro de .NET Fiddle . El mismo código que usa Noda Time 1.x funciona bien. La excepción lanzada es esta:

Se violaron las reglas de seguridad de herencia al anular el 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 al que apunta: 1.x apunta a .NET 3.5 (perfil de cliente); 2.x apunta a .NET 4.5. Tienen grandes diferencias en términos de compatibilidad con PCL frente a .NET Core y la estructura de archivos del proyecto, pero parece que esto es irrelevante.

Me las arreglé para 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 destinada a .NET 4.5.1. Lo llamé "CodeRunner".
  • En las propiedades del proyecto, vaya a Firma y firme el ensamblado con una nueva clave. Desmarque el requisito de contraseña y use cualquier nombre de archivo de clave.
  • Pegue el siguiente código para reemplazarlo Program.cs. Ésta es una versión abreviada del código de este ejemplo de Microsoft . He mantenido todas las rutas iguales, 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". Debe ser un proyecto de biblioteca de clases de escritorio clásico.
  • Firmar 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 contento a Code Analysis).
  • Pegue el siguiente código en Class1.cs(sobrescribiendo lo que está allí):

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 da la siguiente excepción (reformateada para facilitar la lectura):

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

Los atributos comentados muestran cosas que he probado:

  • SecurityPermissiones recomendado por dos artículos de MS diferentes ( primero , segundo ), aunque curiosamente hacen cosas diferentes en torno a la implementación de interfaz explícita / implícita
  • SecurityCriticales 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 análisis de código
  • Sin ningún atributo, las reglas de análisis de código están contentas: con cualquiera de los dos SecurityPermissiono SecurityCritical presente, las reglas le dicen que elimine los atributos, a menos que lo tenga AllowPartiallyTrustedCallers. Seguir las sugerencias en cualquier caso no ayuda.
  • Noda Time se le ha AllowPartiallyTrustedCallersaplicado; el ejemplo aquí no funciona con o sin el atributo aplicado.

El código se ejecuta sin excepción si lo agrego [assembly: SecurityRules(SecurityRuleSet.Level1)]al UntrustedCodeensamblado (y descomento el AllowPartiallyTrustedCallersatributo), pero creo que esa es una mala solución al problema que podría obstaculizar otro código.

Admito plenamente que estoy 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 se implementen ISerializabley se sigan usando en entornos como .NET Fiddle?

(Aunque me dirijo 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).

Jon Skeet
fuente
Curiosamente, esta explicación de los cambios en el modelo de seguridad en 4.0 sugiere que simplemente eliminar AllowPartiallyTrustedCallersdebería funcionar, pero no parece hacer una diferencia
Mathias R. Jessen

Respuestas:

56

De acuerdo con MSDN , en .NET 4.0 básicamente no debe usar ISerializablepara código parcialmente confiable, sino que debe usar ISafeSerializationData

Citando de https://docs.microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

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 está marcado con el atributo SecurityCriticalAttribute que evita la ejecución en ensamblados de confianza parcial. Para solucionar esta condición, implemente la interfaz ISafeSerializationData.

Así que probablemente no sea lo que querías escuchar si lo necesitas, pero no creo que haya otra forma de evitarlo mientras sigues usando ISerializable(aparte de volver a la Level1seguridad, que dijiste que no querías).

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

PS2: el SecurityCriticalatributo no funciona porque se ignora cuando el ensamblaje se carga en modo de confianza parcial ( en seguridad de Nivel2 ). Se puede ver en su código de ejemplo, si se depura la targetvariable en ExecuteUntrustedCodela derecha antes de que lo alega, que tendrá IsSecurityTransparenta truey IsSecurityCriticala falseincluso si marca el método con el SecurityCriticalatributo)

Jcl
fuente
Ajá, gracias por la explicación. Es una pena que la excepción sea tan engañosa aquí. Tendré que decidir qué hacer ...
Jon Skeet
@JonSkeet Honestamente, me desharía de la serialización binaria por completo ... pero entiendo que a su base de usuarios puede no gustarle
Jcl
Creo que tendremos que hacer eso, lo que significa pasar a la v3.0. Sin embargo, tiene otros beneficios ... Necesito consultar a la comunidad de Noda Time.
Jon Skeet
12
@JonSkeet por cierto, si está interesado, este artículo explica las diferencias entre el nivel 1 y el nivel 2 de seguridad (y POR QUÉ no funciona)
Jcl
8

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 Nivel 2 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. Creé rápidamente una prueba unitaria; sin embargo, no pude reproducir el problema que experimenté en .NET Fiddle, mientras que el mismo código arrojó "con éxito" 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 utilizada en su proyecto de consumidor, entonces el código parcialmente confiable funciona como se esperaba: es capaz de instanciar una ISerializableimplementación (y un código crítico de seguridad no se puede llamar directamente, pero ver más abajo). O, lo que es aún más ridículo, puede intentar crear la caja de arena nuevamente si no funcionó por primera vez ...

Pero veamos algo de código.

ClassLibrary.dll:

Separemos dos casos: uno para una clase normal con contenido crítico para la seguridad y otro para la ISerializableimplementación:

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 solucionar 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 InternalsVisibleToatributo y prepárese para el proyecto de prueba:

UnitTest.dll (usa NUnit y ClassLibrary):

Para utilizar 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, me tomó un par de días encontrar un método en una clase de prueba aleatoria.

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 de ISerializable

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

  • InternalTypeReferenceAttribute Está aplicado
  • 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, se produce la Inheritance security rules violated while overriding member...excepción totalmente inapropiada al crear una instancia 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 para la seguridad

La prueba pasa en las mismas condiciones que la primera. Sin embargo, el problema es completamente diferente aquí: un código parcialmente confiable puede acceder directamente a un miembro crítico para la 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 plena confianza del caso 1-2

En aras de la integridad, aquí están los mismos casos que los anteriores ejecutados en un dominio de plena confianza. 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 me sorprendería mucho 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 esta tontería? El ISafeSerializationDataclaramente no es una solución para cualquier cosa: se utiliza exclusivamente por la base de Exceptionclase y si se suscribe al SerializeObjectStateevento (¿por qué no es que un método reemplazable?), Entonces el estado también será consumido por el Exception.GetObjectDataal final.

El AllowPartiallyTrustedCallers/ SecurityCritical/ SecuritySafeCriticaltriunvirato de atributos fue diseñado exactamente para el uso que se muestra arriba. Me parece una tontería total que un código de confianza parcial ni siquiera pueda instanciar un tipo, independientemente del intento de utilizar 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 de plena confianza.

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 SecurityRuleSet.Level1hasta que Microsoft lo solucione.


Actualización: Un billete comunidad de desarrolladores ha sido creado para el tema.

György Kőszeg
fuente
2

Según MSDN, consulte:

¿Cómo corregir las infracciones?

Para corregir una infracción de esta regla, haga que el método GetObjectData sea visible y anulable 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 ejemplo corrige las dos violaciones anteriores proporcionando una implementación anulable de ISerializable.GetObjectData en la clase Book y proporcionando 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);
        }
    }
}
5377037
fuente
2
El artículo al que vinculó es para CA2240, que no se activa, el código no lo viola. Es una estructura, por lo que está efectivamente sellada; no tiene campos; se implementa GetObjectDataexplícitamente, pero hacerlo implícitamente no ayuda.
Jon Skeet
15
Claro, y gracias por intentarlo, pero estoy explicando por qué no funciona. (Y como recomendación, para algo complicado como esto, donde la pregunta incluye un ejemplo verificable, es una buena idea intentar aplicar la solución sugerida y ver si realmente ayuda).
Jon Skeet