¿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
oSecurityCritical
presente, las reglas le indican que elimine los atributos, a menos que tengaAllowPartiallyTrustedCallers
. 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.