How can I implement ISerializable in .NET 4+ without violating inheritance security rules?

Jon Skeet picture Jon Skeet · Jan 20, 2018 · Viewed 7.9k times · Source

Background: Noda Time contains many serializable structs. While I dislike binary serialization, we received many requests to support it, back in the 1.x timeline. We support it by implementing the ISerializable interface.

We've received a recent issue report of Noda Time 2.x failing within .NET Fiddle. The same code using Noda Time 1.x works fine. The exception thrown is this:

Inheritance security rules violated while overriding member: 'NodaTime.Duration.System.Runtime.Serialization.ISerializable.GetObjectData(System.Runtime.Serialization.SerializationInfo, System.Runtime.Serialization.StreamingContext)'. Security accessibility of the overriding method must match the security accessibility of the method being overriden.

I've narrowed this down to the framework that's targeted: 1.x targets .NET 3.5 (client profile); 2.x targets .NET 4.5. They have big differences in terms of support PCL vs .NET Core and the project file structure, but it looks like this is irrelevant.

I've managed to reproduce this in a local project, but I haven't found a solution to it.

Steps to reproduce in VS2017:

  • Create a new solution
  • Create a new classic Windows console application targeting .NET 4.5.1. I called it "CodeRunner".
  • In the project properties, go to Signing and sign the assembly with a new key. Untick the password requirement, and use any key file name.
  • Paste the following code to replace Program.cs. This is an abbreviated version of the code in this Microsoft sample. I've kept all the paths the same, so if you want to go back to the fuller code, you shouldn't need to change anything else.

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);
    }  
}
  • Create another project called "UntrustedCode". This should be a Classic Desktop Class Library project.
  • Sign the assembly; you can use a new key or the same one as for CodeRunner. (This is partially to mimic the Noda Time situation, and partly to keep Code Analysis happy.)
  • Paste the following code in Class1.cs (overwriting what's there):

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

Running the CodeRunner project gives the following exception (reformatted for readability):

Unhandled Exception: System.Reflection.TargetInvocationException:
Exception has been thrown by the target of an invocation.
--->
System.TypeLoadException:
Inheritance security rules violated while overriding member:
'UntrustedCode.CustomStruct.System.Runtime.Serialization.ISerializable.GetObjectData(...).
Security accessibility of the overriding method must match the security
accessibility of the method being overriden.

The commented-out attributes show things I've tried:

  • SecurityPermission is recommended by two different MS articles (first, second), although interestingly they do different things around explicit/implicit interface implementation
  • SecurityCritical is what Noda Time currently has, and is what this question's answer suggests
  • SecuritySafeCritical is somewhat suggested by Code Analysis rule messages
  • Without any attributes, Code Analysis rules are happy - with either SecurityPermission or SecurityCritical present, the rules tell you to remove the attributes - unless you do have AllowPartiallyTrustedCallers. Following the suggestions in either case doesn't help.
  • Noda Time has AllowPartiallyTrustedCallers applied to it; the example here doesn't work either with or without the attribute applied.

The code runs without an exception if I add [assembly: SecurityRules(SecurityRuleSet.Level1)] to the UntrustedCode assembly (and uncomment the AllowPartiallyTrustedCallers attribute), but I believe that's a poor solution to the problem that could hamper other code.

I fully admit to being pretty lost when it comes to this sort of security aspect of .NET. So what can I do to target .NET 4.5 and yet allow my types to implement ISerializable and still be used in environments such as .NET Fiddle?

(While I'm targeting .NET 4.5, I believe it's the .NET 4.0 security policy changes that caused the issue, hence the tag.)

Answer

Jcl picture Jcl · Jan 20, 2018

According to the MSDN, in .NET 4.0 basically you should not use ISerializable for partially trusted code, and instead you should use ISafeSerializationData

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

Important

In versions previous to .NET Framework 4.0, serialization of custom user data in a partially trusted assembly was accomplished using the GetObjectData. Starting with version 4.0, that method is marked with the SecurityCriticalAttribute attribute which prevents execution in partially trusted assemblies. To work around this condition, implement the ISafeSerializationData interface.

So probably not what you wanted to hear if you need it, but I don't think there's any way around it while keeping using ISerializable (other than going back to Level1 security, which you said you don't want to).

PS: the ISafeSerializationData docs state that it is just for exceptions, but it doesn't seem all that specific, you may want to give it a shot... I basically can't test it with your sample code (other than removing ISerializable works, but you knew that already)... you'll have to see if ISafeSerializationData suits you enough.

PS2: the SecurityCritical attribute doesn't work because it's ignored when the assembly is loaded in partial trust mode (on Level2 security). You can see it on your sample code, if you debug the target variable in ExecuteUntrustedCode right before invoking it, it'll have IsSecurityTransparent to true and IsSecurityCritical to false even if you mark the method with the SecurityCritical attribute)