Interesting security change in .NET remoting

A while back I made a simple remoting sample to showcase some of my code. This happened to be written in C# and targeting the 1.0 .NET Framework. It was a very simple example with a client, server, and a shared library between the two. The client would get an instance of an object on the server that is defined in the shared library. The client would then call a method on the remoted (MarshalByRef) object, passing in an instance of another object that is also defined in the shared library. This shared library happened to be strong named, whereas the client and server applications were not.

I ran the sample and it worked just fine in the 1.0 .NET Framework. The client would create and pass the objects from the shared library to the server. The code is as follows:

(Shared Library)
namespace RemShared
{
[Serializable]
public class ByValueObject
{
public string SomeValue = string.Empty;
}

public class ByRefObject : MarshalByRefObject
{
public void DoSomethingWith(ByValueObject valObj)
{
Console.WriteLine(valObj.SomeValue);
}
}
}

(Remoting Server)
namespace RemServer
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
TcpChannel channel = new TcpChannel(1111);
ChannelServices.RegisterChannel(channel);

RemShared.ByRefObject bro = new RemShared.ByRefObject();
ObjRef objRef = RemotingServices.Marshal(bro, "remoted.object", typeof(RemShared.ByRefObject));

Console.WriteLine("Server started, press enter to exit...");
Console.ReadLine();

RemotingServices.Unmarshal(objRef);
ChannelServices.UnregisterChannel(channel);
}
}
}

(Remoting Client)
namespace RemClient
{
class Class1
{
[STAThread]
static void Main(string[] args)
{
try
{
TcpChannel channel = new TcpChannel();
ChannelServices.RegisterChannel(channel);

object remoteObject = Activator.GetObject(typeof(RemShared.ByRefObject),
"tcp://localhost:1111/remoted.object");
RemShared.ByRefObject bro = (RemShared.ByRefObject) remoteObject;

RemShared.ByValueObject bvo = new RemShared.ByValueObject();
bvo.SomeValue = "Hello Remoting";

bro.DoSomethingWith(bvo);
}
catch(Exception ex)
{
Console.WriteLine(ex.ToString());
}
finally
{
Console.WriteLine("Finished...");
Console.ReadLine();
}
}
}
}

Very, very simple code. I am using RemotingServices.Marshal for this example just because it's easier. You could rightfully do any other method of exposing a remote object and get the same results.

This works in the .NET 1.0 Framework (remember that the shared library is signed), however the moment you convert it to 1.0 it does not. Take a look in the folder "2. Does not work" in my download below. Since I couldn't get it to work I decided to attempt to take off the signing on a whim. Look at folder "1. Works". Both sets of code are the same except for one difference: the code in "2. Does not work" has the shared library signed. In fact, when you execute the "2. Does not work" code, you get an exception:

System.Runtime.Serialization.SerializationException: Because of security restrictions, the type RemShared.ByValueObject cannot be accessed ---> System.Security.SecurityException: Request failed.

Very, very odd if you ask me. Both console applications are running with full trust, so why would there be a security exception? If you run "caspol -s off" from a VS2k3 Command Prompt then try the "2. Does not work" sample again, the code execute successfully! Why would a full trust application receive a security exception from code access security? I thought for a second that since the shared library was signed that there was some problem because the client and server were not signed, so I signed both the client and server with the same key. That in fact, does not solve the problem (look at "3. Does not work" folder in the download).

Tired off this stupid problem I google for the exception. I come up with
this.

Someone has the same problem as me and someone else suggests applying the System.Security.AllowPartiallyTrustedCallersAttribute attribute to the assembly. I tried this on my shared library (see "4. Works" folder) and it made everything work. Not quite understanding why, I pulled up the documentation for it and get this:

"AllowPartiallyTrustedCallersAttribute is only effective when applied by a strong-named assembly at the assembly level. For more information about applying attributes at the assembly level, see Applying Attributes.

By default, a strong-named assembly that does not explicitly apply this attribute at assembly level to allow its use by partially trusted code can be called only by other assemblies that are granted full trust by security policy. This restriction is enforced by placing a LinkDemand for FullTrust on every public or protected method on every publicly accessible class in the assembly. Assemblies that are intended to be called by partially trusted code can declare their intent through the use of the AllowPartiallyTrustedCallersAttribute.
"

From what I see here, is that anything that executes and doesn't have FullTrust applied will raise an exception without this attribute. Well, I am running two console apps from the command line with an administrator account. Does that not give me FullTrust? I'm pretty sure it does. So I took a look at the breaking security changes from 1.0 to 1.1. I come up with this (which is also pointed to from some google groups searches). From what I see, basically it says anything can be remoted that implements ISerializable or has the SerializableAttribute applied to it (or that is a primitive type). It says that ObjRefs, ISponsors, and anything inserted between the proxy and client by the IContributeEnvoySink interface cannot be remoted by default and must set the "typeFilterLevel" to full. This doesn't really sound like it applies, but I tried it anyway but to no avail. Some people on some newsgroups suggest that this is the change in 1.1 that is causing this error, but I'm not so sure.

So really I am 100% unsure as to why the hell I receive this exception, as it doesn't seem to make much sense. I do not have a partially trusted application in either my client or my server. My strongly-named assembly is able to be loaded, which means I am in a fully trusted environment (according to the documentation, something that is not fully trusted cannot load a strongly named assembly that doesn't have the AllowPartiallyTrustedCallerAttribute applied to it). However, I cannot remote an object from one fully trusted application to another without applying this attribute.

Is this a bug? Can someone explain this? My example code is here and is runnable if you have the .NET Framework 1.1 installed. You can see the code with notepad or open the solution/project files with VS2k3. All I know now is that at work we have all signed assemblies and we need to remote quite a few of them, so we have to apply the AllowPartiallyTrustedCallersAttribute to every one of them, which doesn't quite make sense to me.

3 comment(s)

joshua wrote on January 23, 2008

Bear with me, I definitely not an expert on the subject.

I would imagine that the calling code is executing in a context that you're not expecting. The CLR doesn't really rely on the security context of the user, rather the security context of the zone from which the code comes.

In this case, the code is coming from the local intranet zone and is thus considered partially trusted code. As far as I know, there is no way to programmatically allow the code access without AllowPartiallyTrustedCallersAttribute. I think you could still add your own LinkDemand to require that the calling code be signed with your key, just to work around the security worries that AllowPartiallyTrustedCallers presents.

The only thing that troubles me about this theory is that it worked in 1.0, and now doesn't. I don't know whether the restriction was present in 1.0, so I could be way off base here.

The best resource I've found for an explanation of the code security infrastructure is here. It has got some helpful code snippets in there to help further diagnose the issue.

I've done a ton of research since I wrote this:

V1.1 of the framework added some more security into remoting, and there is one particular exception that doesn't seem to make much sense but comes up a lot.

Basically the situation is when you have a remoting client and server and the client serializes custom objects to send to the server that live in a strongly named assembly. Anything more than very primitive types (string, int, etc) requires the assembly the serialized objects live in to have the AllowPartiallyTrustedCallersAttribute applied to it, or have its typeFilterLevel set to Full.

The idea behind it is that as a library developer of a strongly named assembly, I have to explicitly permit runtime serialization through remoting of my objects. If I did not explicitly permit it by using the APTC attribute, a System.Runtime.Serialization.SerializationException is thrown with a message similar to:

"System.Runtime.Serialization.SerializationException: Because of security restrictions, the type {your type name} cannot be accessed".

This forces the application developer to determine if their objects are "safe" for remoting. The reason they could be unsafe is that a malicious "attacker" could potentially hijack the deserialization process and inject unwanted code. If your assembly is not strongly named, the runtime assumes it is unimportant to do the verification and allows your types to be serialized/deserialized via remoting without the APTC attribute.

The other exception you can run into is this:

"System.Runtime.Serialization.SerializationException: Because of security restrictions, the type ObjRef cannot be accessed".

This exception is thrown when you attempt to remote an ObjRef or some other objects (I don't exactly know which types). If you get this, all you need to do is set the typeFilterLevel to Full.

Hashtable props = new Hashtable();
props.Add("port", 0);
props.Add("typeFilterLevel", "Full");

TcpChannel channel = new TcpChannel(props, ...);

The typeFilterLevel property basically indicates that some types (and this is irregardless of whether your assembly is strongly named or not) are simply not supported unless you specifically tell .NET remoting to support them. Low (the default) allows you to deserialize almost any type, and Full allows you to deserialize ObjRef, ISponsor and IContributeEnvoySink objects.

Oh yeah, and this doesn't apply just to remoting serialization, but to serialization in general (I've come across is when serializing objects in MSMQ also)