Como posso implementar ISerializable no .NET 4+ sem violar as regras de segurança de herança?

109

Fundo: Noda Time contém muitas estruturas serializáveis. Embora eu não goste da serialização binária, recebemos muitas solicitações de suporte, na linha do tempo 1.x. Nós o apoiamos implementando a ISerializableinterface.

Recebemos um relatório de problema recente do Noda Time 2.x falha no .NET Fiddle . O mesmo código usando Noda Time 1.x funciona bem. A exceção lançada é esta:

Regras de segurança de herança violadas ao substituir o membro: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData (System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. A acessibilidade de segurança do método de substituição deve corresponder à acessibilidade de segurança do método que está sendo substituído.

Limitei isso à estrutura que é direcionada: 1.x direciona o .NET 3.5 (perfil de cliente); 2.x visa .NET 4.5. Eles têm grandes diferenças em termos de suporte PCL vs .NET Core e a estrutura de arquivos do projeto, mas parece que isso é irrelevante.

Consegui reproduzir isso em um projeto local, mas não encontrei uma solução para isso.

Passos para reproduzir no VS2017:

  • Crie uma nova solução
  • Crie um novo aplicativo de console clássico do Windows direcionado ao .NET 4.5.1. Eu o chamei de "CodeRunner".
  • Nas propriedades do projeto, acesse Assinatura e assine a montagem com uma nova chave. Desmarque o requisito de senha e use qualquer nome de arquivo de chave.
  • Cole o código a seguir para substituir Program.cs. Esta é uma versão abreviada do código neste exemplo da Microsoft . Eu mantive todos os caminhos iguais, então se você quiser voltar para o código mais completo, não deve precisar alterar mais nada.

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);
    }  
}
  • Crie outro projeto chamado "UntrustedCode". Este deve ser um projeto Classic Desktop Class Library.
  • Assine a assembleia; você pode usar uma nova chave ou a mesma do CodeRunner. (Isso é parcialmente para imitar a situação do Tempo Noda e, em parte, para manter a Análise de Código feliz.)
  • Cole o seguinte código Class1.cs(substituindo o que está lá):

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

Executar o projeto CodeRunner oferece a seguinte exceção (reformatado para facilitar a leitura):

Exceção não tratada: System.Reflection.TargetInvocationException: a
exceção foi lançada pelo destino de uma invocação.
--->
System.TypeLoadException:
Regras de segurança de herança violadas ao substituir o membro:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData (...).
A acessibilidade de segurança do método de substituição deve corresponder à
acessibilidade de segurança do método que está sendo substituído.

Os atributos comentados mostram coisas que tentei:

  • SecurityPermissioné recomendado por dois artigos diferentes de MS ( primeiro , segundo ), embora curiosamente eles façam coisas diferentes em torno da implementação de interface explícita / implícita
  • SecurityCriticalé o que Noda Time tem atualmente, e é o que a resposta desta pergunta sugere
  • SecuritySafeCritical é algo sugerido por mensagens de regra de análise de código
  • Sem quaisquer atributos, as regras de análise de código ficam satisfeitas - com um SecurityPermissionou SecurityCritical presente, as regras dizem para você remover os atributos - a menos que você os tenha AllowPartiallyTrustedCallers. Seguir as sugestões em ambos os casos não ajuda.
  • O Tempo de Noda se AllowPartiallyTrustedCallersaplicou a ele; o exemplo aqui não funciona com ou sem o atributo aplicado.

O código é executado sem exceção se eu adicionar [assembly: SecurityRules(SecurityRuleSet.Level1)]ao UntrustedCodeassembly (e descomentar o AllowPartiallyTrustedCallersatributo), mas acredito que essa seja uma solução ruim para o problema que pode atrapalhar outro código.

Eu admito totalmente que estou muito perdido quando se trata desse tipo de aspecto de segurança do .NET. Então, o que posso fazer para direcionar o .NET 4.5 e ainda permitir que meus tipos implementem ISerializablee ainda sejam usados ​​em ambientes como o .NET Fiddle?

(Embora meu objetivo seja o .NET 4.5, acredito que foram as mudanças na política de segurança do .NET 4.0 que causaram o problema, daí a tag.)

Jon Skeet
fonte
Curiosamente, esta explicação das mudanças no modelo de segurança em 4.0 sugere que simplesmente remover AllowPartiallyTrustedCallersdeve resolver, mas não parece fazer diferença
Mathias R. Jessen

Respostas:

56

De acordo com o MSDN , no .NET 4.0 basicamente você não deve usar ISerializablepara código parcialmente confiável e, em vez disso, deve usar ISafeSerializationData

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

Importante

Nas versões anteriores ao .NET Framework 4.0, a serialização de dados personalizados do usuário em um assembly parcialmente confiável era realizada usando GetObjectData. A partir da versão 4.0, esse método é marcado com o atributo SecurityCriticalAttribute que evita a execução em assemblies parcialmente confiáveis. Para contornar essa condição, implemente a interface ISafeSerializationData.

Provavelmente não é o que você gostaria de ouvir se precisar, mas não acho que haja como evitar enquanto continua usando ISerializable(além de voltar à Level1segurança, que você disse que não queria).

PS: os ISafeSerializationDatadocumentos afirmam que é apenas para exceções, mas não parece tão específico, você pode querer tentar ... Basicamente, não posso testá-lo com o seu código de amostra (exceto removerISerializable obras, mas você já sabia disso) ... você terá que ver se ISafeSerializationDatacombina com você o suficiente.

PS2: o SecurityCriticalatributo não funciona porque é ignorado quando o assembly é carregado no modo de confiança parcial ( na segurança Nível2 ). Você pode ver isso em seu código de amostra, se você depurar a targetvariável ExecuteUntrustedCodeantes de invocá-la, terá que IsSecurityTransparentfazer truee IsSecurityCriticalaté falsemesmo se você marcar o método com o SecurityCriticalatributo)

Jcl
fonte
Aha - obrigado pela explicação. É uma pena que a exceção seja tão enganosa aqui. Precisará decidir o que fazer ...
Jon Skeet
@JonSkeet Honestamente, eu dispensaria a serialização binária por completo ... mas entendo que sua base de usuários pode não gostar
Jcl
Acho que teremos que fazer isso - o que significa mudar para a v3.0. Mas tem outros benefícios ... Vou precisar consultar a comunidade Noda Time.
Jon Skeet
12
@JonSkeet btw, se você estiver interessado, este artigo explica as diferenças entre o nível 1 e o nível 2 de segurança (e POR QUE isso não funciona)
Jcl
8

A resposta aceita é tão convincente que quase acreditei que não era um bug. Mas, depois de fazer alguns experimentos, posso dizer que a segurança do Nível 2 é uma bagunça completa; pelo menos, algo é realmente suspeito.

Alguns dias atrás, encontrei o mesmo problema com minhas bibliotecas. Rapidamente criei um teste de unidade; no entanto, não consegui reproduzir o problema que experimentei no .NET Fiddle, enquanto o mesmo código "com êxito" lançou a exceção em um aplicativo de console. No final, descobri duas maneiras estranhas de superar o problema.

TL; DR : Acontece que, se você usar um tipo interno da biblioteca usada em seu projeto de consumidor, o código parcialmente confiável funciona conforme o esperado: é capaz de instanciar uma ISerializableimplementação (e um código crítico de segurança não pode ser chamado diretamente, mas veja abaixo). Ou, o que é ainda mais ridículo, você pode tentar criar a sandbox novamente se não funcionar pela primeira vez ...

Mas vamos ver algum código.

ClassLibrary.dll:

Vamos separar dois casos: um para uma aula regular com conteúdo crítico de segurança e um ISerializable implementação:

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

Uma maneira de superar o problema é usar um tipo interno do conjunto do consumidor. Qualquer tipo fará isso; agora eu defino um atributo:

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

E os atributos relevantes aplicados à montagem:

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

Assine a montagem, aplique a chave ao InternalsVisibleTo atributo e prepare-se para o projeto de teste:

UnitTest.dll (usa NUnit e ClassLibrary):

Para usar o truque interno, o assembly de teste também deve ser assinado. Atributos de montagem:

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

Observação : o atributo pode ser aplicado em qualquer lugar. No meu caso, foi em um método em uma aula de teste aleatória que me levou alguns dias para encontrar.

Nota 2 : Se você executar todos os métodos de teste juntos, pode acontecer que os testes sejam aprovados.

O esqueleto da classe de teste:

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

E vamos ver os casos de teste um por um

Caso 1: Implementação serializável

O mesmo problema da pergunta. O teste passa se

  • InternalTypeReferenceAttribute é aplicado
  • o sandbox é tentado para ser criado várias vezes (consulte o código)
  • ou, se todos os casos de teste forem executados de uma vez e este não for o primeiro

Do contrário, surge a Inheritance security rules violated while overriding member...exceção totalmente inadequada ao instanciar 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: aula regular com membros críticos de segurança

O teste passa nas mesmas condições do primeiro. No entanto, o problema é completamente diferente aqui: um código parcialmente confiável pode acessar um membro crítico de segurança diretamente .

[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: versões de confiança total do caso 1-2

Para fins de integridade, aqui estão os mesmos casos que os anteriores executados em um domínio totalmente confiável. Se você remover [assembly: AllowPartiallyTrustedCallers]os testes, falhará porque poderá acessar o código crítico diretamente (já que os métodos não são mais transparentes por padrão).

[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:

Claro, isso não resolverá seu problema com o .NET Fiddle. Mas agora eu ficaria muito surpreso se não fosse um bug no framework.

A maior questão para mim agora é a parte citada na resposta aceita. Como eles chegaram a esse absurdo? O ISafeSerializationDataclaramente não é uma solução para nada: é usado exclusivamente pela Exceptionclasse base e se você inscrever o SerializeObjectStateevento (por que não é um método substituível?), Então o estado também será consumido peloException.GetObjectData no final.

O AllowPartiallyTrustedCallers/ SecurityCritical/ SecuritySafeCriticaltriunvirato de atributos foi projetado exatamente para o uso mostrado acima. Parece totalmente absurdo para mim que um código parcialmente confiável não pode nem mesmo instanciar um tipo, independentemente da tentativa de usar seus membros críticos de segurança. Mas é um absurdo ainda maior (uma falha de segurança na verdade) que um código parcialmente confiável pode acessar um método crítico de segurança diretamente (ver caso 2 ), ao passo que isso é proibido para métodos transparentes, mesmo de um domínio totalmente confiável.

Portanto, se seu projeto de consumidor for um teste ou outro assembly bem conhecido, o truque interno pode ser usado perfeitamente. Para .NET Fiddle e outros ambientes de área restrita da vida real, a única solução é voltar a SecurityRuleSet.Level1isso até que isso seja corrigido pela Microsoft.


Atualização: um tíquete da comunidade de desenvolvedores foi criado para o problema.

György Kőszeg
fonte
2

De acordo com o MSDN, consulte:

Como corrigir violações?

Para corrigir uma violação dessa regra, torne o método GetObjectData visível e substituível e certifique-se de que todos os campos de instância sejam incluídos no processo de serialização ou marcados explicitamente com o atributo NonSerializedAttribute .

O exemplo a seguir corrige as duas violações anteriores fornecendo uma implementação substituível de ISerializable.GetObjectData na classe Book e fornecendo uma implementação de ISerializable.GetObjectData na 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);
        }
    }
}
5377037
fonte
2
O artigo ao qual você fez um link é para CA2240, que não é disparado - o código não o viola. É uma estrutura, portanto, é efetivamente selada; não tem campos; ele implementa GetObjectDataexplicitamente, mas fazê-lo implicitamente não ajuda.
Jon Skeet de
15
Claro, e obrigado por tentar - mas estou explicando por que não funciona. (E como recomendação - para algo complicado como isso, em que a pergunta inclui um exemplo verificável, é uma boa ideia tentar aplicar a correção sugerida e ver se ela realmente ajuda.)
Jon Skeet