web-dev-qa-db-fra.com

Comment puis-je implémenter ISerializable dans .NET 4+ sans violer les règles de sécurité de l'héritage?

Contexte: Noda Time contient de nombreuses structures sérialisables. Bien que je n'aime pas la sérialisation binaire, nous avons reçu de nombreuses demandes de prise en charge, dans la chronologie 1.x. Nous le supportons en implémentant l'interface ISerializable.

Nous avons reçu un récent problème de Noda Time 2.x échec dans .NET Fiddle . Le même code utilisant Noda Time 1.x fonctionne bien. L'exception levée est la suivante:

Règles de sécurité d'héritage violées lors du remplacement du membre: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. L'accessibilité de la méthode de substitution par la sécurité doit correspondre à celle de la méthode en cours de substitution.

J'ai réduit cela au framework ciblé: 1.x cibles .NET 3.5 (profil client); 2.x cibles .NET 4.5. Ils ont de grandes différences en termes de prise en charge PCL vs .NET Core et de structure de fichier de projet, mais il semble que ce ne soit pas pertinent.

J'ai réussi à reproduire cela dans un projet local, mais je n'ai pas trouvé de solution.

Les étapes pour reproduire dans VS2017:

  • Créer une nouvelle solution
  • Créez une nouvelle application de console Windows classique ciblant .NET 4.5.1. Je l'ai appelé "CodeRunner".
  • Dans les propriétés du projet, accédez à Signature et signez l’assemblage avec une nouvelle clé. Décochez la condition de mot de passe et utilisez n'importe quel nom de fichier de clé.
  • Collez le code suivant pour remplacer Program.cs. Ceci est une version abrégée du code présent dans cet exemple Microsoft . J'ai gardé tous les chemins identiques, donc si vous voulez revenir au code plus complet, vous ne devriez rien changer d'autre.

Code:

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);
    }  
}
  • Créez un autre projet appelé "UntrustedCode". Cela devrait être un projet de bibliothèque de classes Classic Desktop.
  • Signer l'assemblée; vous pouvez utiliser une nouvelle clé ou la même que celle utilisée pour CodeRunner. (Ceci est en partie pour imiter la situation de Noda Time et en partie pour garder l'analyse de code heureuse.)
  • Collez le code suivant dans Class1.cs (Écrasant ce qui est là):

Code:

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

L'exécution du projet CodeRunner donne l'exception suivante (reformatée pour la lisibilité):

Exception non gérée: System.Reflection.TargetInvocationException:
Une exception a été émise par la cible d'une invocation.
--->
System.TypeLoadException:
Règles de sécurité relatives à l'héritage violées lors de la substitution d'un membre:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
L'accessibilité de la méthode de substitution à la sécurité doit correspondre à la sécurité
accessibilité de la méthode à annuler.

Les attributs commentés montrent les choses que j'ai essayées:

  • SecurityPermission est recommandé par deux articles MS différents ( premier , deuxième ), bien qu'il soit intéressant qu'ils fassent des choses différentes autour de l'implémentation d'interface explicite/implicite
  • SecurityCritical est ce que Noda Time a actuellement, et est ce que réponse de cette question suggère
  • SecuritySafeCritical est suggéré par les messages de la règle d'analyse de code
  • Sans aucun attribut , les règles d'analyse de code sont satisfaisantes - avec SecurityPermission ou SecurityCritical présent, les règles vous indiquent supprime les attributs - sauf si vous faites avez AllowPartiallyTrustedCallers. Suivre les suggestions dans les deux cas n'aide pas.
  • Noda Time a appliqué AllowPartiallyTrustedCallers; l'exemple ici ne fonctionne pas avec ou sans l'attribut appliqué.

Le code s'exécute sans exception si j'ajoute [Assembly: SecurityRules(SecurityRuleSet.Level1)] à l'assembly UntrustedCode (et supprime le commentaire de l'attribut AllowPartiallyTrustedCallers), mais j'estime qu'il s'agit d'une mauvaise solution au problème qui pourrait entraver d'autres code.

J'admets pleinement être assez perdu quand il s'agit de cet aspect de la sécurité de .NET. Donc, que peut je faire pour cibler .NET 4.5 tout en permettant à mes types d’implémenter ISerializable tout en restant utilisé dans des environnements tels que. NET Violon?

(Bien que je cible .NET 4.5, je pense que ce sont les modifications de la politique de sécurité .NET 4.0 qui ont provoqué le problème, d'où le tag.)

91
Jon Skeet

Selon le MSDN , dans .NET 4.0, vous ne devriez pas utiliser essentiellement ISerializable pour le code partiellement sécurisé, mais plutôt ISafeSerializationData

Citant de https://docs.Microsoft.com/en-us/dotnet/standard/serialization/custom-serialization

Important

Dans les versions antérieures à .NET Framework 4.0, la sérialisation des données utilisateur personnalisées dans un assembly partiellement sécurisé était réalisée à l'aide de GetObjectData. À partir de la version 4.0, cette méthode est marquée avec l'attribut SecurityCriticalAttribute qui empêche l'exécution dans des assemblys partiellement approuvés. Pour contourner cette condition, implémentez l'interface ISafeSerializationData.

Donc, probablement pas ce que vous vouliez entendre si vous en avez besoin, mais je ne pense pas qu'il soit possible de le contourner tout en continuant à utiliser ISerializable (à part revenir à Level1 _ sécurité, ce que vous avez dit ne pas vouloir).

PS: les documents ISafeSerializationData indiquent qu'il ne s'agit que d'exceptions, mais cela ne semble pas si spécifique, vous voudrez peut-être essayer ... Je ne peux pas le tester avec votre exemple de code (à part enlever ISerializable fonctionne, mais vous le saviez déjà) ... vous devrez voir si ISafeSerializationData vous convient assez.

PS2: l'attribut SecurityCritical ne fonctionne pas car il est ignoré lorsque l'assembly est chargé en mode de confiance partiel ( sur la sécurité de niveau 2 ). Vous pouvez le voir sur votre exemple de code, si vous déboguez la variable target dans ExecuteUntrustedCode juste avant de l'invoquer, il aura IsSecurityTransparent à true et IsSecurityCritical à false même si vous marquez la méthode avec l'attribut SecurityCritical

46
Jcl

La réponse acceptée est tellement convaincante que j'ai failli croire que ce n'était pas un bug. Mais après avoir fait quelques expériences maintenant, je peux dire que la sécurité de Level2 est un gâchis total; au moins, quelque chose est vraiment louche.

Il y a quelques jours, j'ai rencontré le même problème avec mes bibliothèques. J'ai rapidement créé un test unitaire. Cependant, je ne pouvais pas reproduire le problème rencontré dans .NET Fiddle, alors que le même code "lançait" avec succès l'exception dans une application console. En fin de compte, j'ai trouvé deux manières étranges de surmonter le problème.

TL; DR : Il s’avère que si vous utilisez un type interne de la bibliothèque utilisée dans votre projet consommateur, le code partiellement sécurisé fonctionne comme prévu: il est capable d'instancier une implémentation de ISerializable (et un code critique pour la sécurité ne peut pas être appelé directement, mais voir ci-dessous). Ou, ce qui est encore plus ridicule, vous pouvez essayer de recréer le bac à sable si cela ne fonctionnait pas pour la première fois ...

Mais voyons du code.

ClassLibrary.dll:

Séparons deux cas: un pour une classe ordinaire avec un contenu critique pour la sécurité et un autre 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) { }
}

Un moyen de résoudre ce problème consiste à utiliser un type interne issu de l’Assemblée consommateur. Tout type le fera; maintenant je définis un attribut:

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

Et les attributs pertinents appliqués à l'Assemblée:

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

Signez l’ensemble, appliquez la clé à l’attribut InternalsVisibleTo et préparez-vous pour le projet test:

UnitTest.dll (utilise NUnit et ClassLibrary):

Pour utiliser l'astuce interne, le test Assembly doit également être signé. Attributs d'assemblage:

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

Remarque : cet attribut peut être appliqué n’importe où. Dans mon cas, c'était sur une méthode dans une classe de tests aléatoires qui m'a pris quelques jours à trouver.

Remarque 2 : Si vous exécutez toutes les méthodes de test ensemble, il peut arriver que les tests réussissent.

Le squelette de la classe de test:

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

Et voyons les cas un par un

Cas 1: implémentation ISerializable

Le même problème que dans la question. Le test réussit si

  • InternalTypeReferenceAttribute est appliqué
  • le bac à sable est essayé pour être créé plusieurs fois (voir le code)
  • ou, si tous les cas de test sont exécutés en même temps et que ce n'est pas le premier

Sinon, il y a le totalement inapproprié Inheritance security rules violated while overriding member... _ exception lorsque vous instanciez 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);
    }

}

Cas 2: cours normal avec des membres critiques pour la sécurité

Le test réussit dans les mêmes conditions que le premier. Cependant, le problème est complètement différent ici: un code partiellement sécurisé peut accéder directement à un membre essentiel pour la sécurité.

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

Cas 3-4: Versions de confiance totale du cas 1-2

Par souci d'exhaustivité, voici les mêmes cas que ceux ci-dessus exécutés dans un domaine entièrement sécurisé. Si vous supprimez [Assembly: AllowPartiallyTrustedCallers] les tests échouent car vous pouvez accéder directement au code critique (car les méthodes ne sont plus transparentes par défaut).

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

Épilogue:

Bien sûr, cela ne résoudra pas votre problème avec .NET Fiddle. Mais maintenant, je serais très surpris que ce ne soit pas un bug dans le framework.

La plus grande question qui m'est posée maintenant concerne la partie citée dans la réponse acceptée. Comment sont-ils sortis avec ce non-sens? Le ISafeSerializationData n'est clairement pas une solution: il est utilisé exclusivement par la classe de base Exception et si vous souscrivez l'événement SerializeObjectState (pourquoi n'est-ce pas une méthode redéfinissable ?), alors l'état sera également consommé par le Exception.GetObjectData à la fin.

Le triumvirat d'attributs AllowPartiallyTrustedCallers/SecurityCritical/SecuritySafeCritical a été conçu exactement pour l'utilisation indiquée ci-dessus. Il me semble totalement absurde qu'un code partiellement sécurisé ne puisse même pas instancier un type, quelle que soit la tentative d'utilisation de ses membres critiques pour la sécurité. Mais c’est un non-sens encore plus grand (un trou de sécurité en fait) qu’un code partiellement sécurisé peut accéder directement à une méthode critique de sécurité (voir le cas 2 ) alors que cela est interdit pour les méthodes transparentes, même à partir d'un domaine entièrement approuvé.

Donc, si votre projet consommateur est un test ou un autre assemblage connu, l’astuce interne peut être utilisée à la perfection. Pour .NET Fiddle et d’autres environnements réels en mode bac à sable, la seule solution est de revenir à SecurityRuleSet.Level1 jusqu'à ce que ceci soit corrigé par Microsoft.


Mise à jour: Un ticket de la communauté de développeurs a été créé pour le problème.

3
György Kőszeg

Selon le MSDN voir:

Comment réparer les violations?

Pour corriger une violation de cette règle, définissez la méthode GetObjectData de manière visible et substituable, et assurez-vous que tous les champs d'instance sont inclus dans le processus de sérialisation ou explicitement marqués avec l'attribut NonSerializedAttribute .

Le exemple suivant corrige les deux violations précédentes en fournissant une implémentation remplaçable de ISerializable.GetObjectData sur la classe Book et en fournissant une implémentation de ISerializable.GetObjectData sur la classe 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);
        }
    }
}
2
5377037