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)
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?
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 toException
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 requestedException
-derived type. There’s no middle ground where the deserializer says “well, I found aType
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.