runtime: Consider allowing Exception instances to be serialized without requiring BinaryFormatter

Some remoting technologies use BinaryFormatter to serialize Exception instances across security boundaries, which puts them out of SDL compliance and potentially exposes consumers to security vulnerabilities. The current recommended way to serialize exception information safely is to call ToString on the exception instance, then transmit the resulting string across the wire. However, this does not create useful object models for consuming applications, as they can’t interact with a simple string like they can a rich exception instance (accessing properties, using try / catch, etc.).

In an ideal world, an exception serialization tech would have the following characteristics:

  • The proper Exception-derived type will be instantiated after deserialization.
  • The human-readable message and stack trace are preserved.
  • The inner exceptions are preserved.
  • Vital information about the exception is preserved. This is normally information passed to the Exception ctor and exposed via properties, such as the argument name provided to an ArgumentOutOfRangeException.

To maintain SDL compliance and work with our linker technology, we’d need to enforce a few extra behaviors:

  • The payload cannot be the sole arbiter of type information. The deserializer needs to provide an allow list of legal Exception-derived types, and the payload cannot attempt to instantiate types outside that allowed list
  • The deserializer tech must go through normal instance validation and type safety checks, normally performed by the exception’s ctor. This disallows using the existing SerializationInfo / StreamingContext infrastructure.
  • Cyclic or deeply-nested object graphs must not be constructed. This also disallows using the existing SerializationInfo / StreamingContext infrastructure.

It’s possible that the deserialization tech would need to include special-case handling of each allowed Exception-derived type in order to fulfill these requirements. Perhaps this could be simplified by understanding canonical patterns like .ctor(string message, Exception innerException). But we’ll cross that bridge when we come to it.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 8
  • Comments: 16 (13 by maintainers)

Most upvoted comments

For those who don’t know what some TLAs mean, like me, I think SDL refers to Microsoft Security Development Lifecycle.

A 100% faithful reconstruction of the original Exception instance is not necessarily required. For example, perhaps it’s good enough for our serializer to say that it doesn’t deal with serializing the Data dictionary. (If we were to try to handle that, we’d become a serializer for arbitrary data, and the .NET ecosystem does not need yet another arbitrary data serializer.) This also means that we’d lose Watson bucket info. But we’d definitely want to keep useful information like the human-readable stack trace around. Restoring that data might involve the creation of new API surface within the runtime.

That still relies on the Remoting infrastructure doing the serialization, and has been somewhat generalized with ExceptionDispatchInfo since .NET 4.5.

Perhaps being able to serialize a ExceptionDispatchInfo would solve this problem?

What would happen in the case where the deserializing assembly/service/app/whatever does not know about or have access to the appropriate Exception subclass?

The case of “not having access” shouldn’t be possible with this design. The deserializer needs to have ahead-of-time knowledge about all possible Exception classes that might be instantiated. (This characteristic is true of any secure polymorphic deserialization technology; it’s not unique to Exception and derived types.)

This means that deserialization scenarios fall into only two buckets: (1) the deserializer is aware of the requested Exception-derived type and knows how to instantiate it; or (2) the deserializer is not aware of the requested Exception-derived type. There’s no middle ground where the deserializer says “well, I found a Type that corresponds to what the payload is requesting, but I don’t know how to instantate it.”

In the case of the second bucket above, I imagine the deserializer would instantiate a placeholder Exception-derived type and include the original error message and the original stack trace.