This post is part of the series 'Vulnerabilities'. Be sure to check out the rest of the blog posts of the series!
Serialization is the process of converting an object into a stream of bytes to store the object or transmit it to memory, a database, or a file. It is very useful for persisting data and loading it later, or for transferring data between two systems. The .NET framework comes with multiple serializers including BinaryFormatter, JavaScriptSerializer, XmlSerializer, and DataContractSerializer. While serializing and deserializing data is straightforward, you should pay close attention to the options provided by the deserializer to avoid introducing vulnerabilities in your code.
For instance, you can use the BinaryFormatter to persist an object to a file.
C#
static void Main(string[] args)
{
var o = new Sample { Path = "test" };
using (var stream = File.OpenWrite("output.bin"))
{
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Serialize(stream, collection);
}
using (var stream = File.OpenRead("output.bin"))
{
var binaryFormatter = new BinaryFormatter();
var deserialized = binaryFormatter.Deserialize(stream);
}
}
[Serializable]
class Sample
{
public string Path { get; set; }
}

#Why is it dangerous?
However, the deserializer does not validate that the file has not been tampered with. If you send a file containing binary serialized data to a server, an attacker can craft a payload that creates an instance of any type on the server. The deserializer will instantiate the object and assign values to its properties. This is a problem when constructors or property setters execute malicious code. One attack vector is to instantiate a class that implements IDisposable, such as TempFileCollection. This class deletes its registered files when disposed, so once the garbage collector calls the finalizer, those files are deleted. More critically, an attacker could execute arbitrary commands on the machine. This is known as Remote Code Execution (RCE). The BinaryFormatter has been the root cause of several real-world vulnerabilities, including those in Microsoft Exchange Server (CVE-2018-8302) and Docker (CVE-2018-15514).
To sum up, deserialization executes code from different locations:
- The constructor of each deserialized instance
- The getter/setter of each property
- The destructor when the garbage collector (GC) destroys the instance
#How to prevent that attack with common .NET serializers?
You can prevent this by implementing a custom SerializationBinder to only allow a reduced set of types to be instantiated:
C#
using (var stream = File.OpenRead("output.bin"))
{
var binaryFormatter = new BinaryFormatter();
binaryFormatter.Binder = new CustomBinder();
var deserialized = binaryFormatter.Deserialize(stream);
}
// Only allow known-types to be instantiated
class CustomBinder : SerializationBinder
{
public override Type BindToType(string assemblyName, string typeName)
{
if (assemblyName == typeof(Sample).Assembly.FullName && typeName == typeof(Sample).FullName)
return typeof(Sample);
throw new ArgumentOutOfRangeException();
}
}
If you are using ASP.NET, you may know that ViewState can store any serializable object, which means it can also deserialize those objects. ASP.NET uses the BinaryFormatter to store ViewState, so a malicious payload could create an instance of any type on your server. You can prevent this by setting enableViewStateMac to true (enabled by default). This ensures the payload was generated by the server before it is deserialized. You can read more about this parameter on the ASP.NET blog.
Other serializers may also be susceptible to this attack. For instance, if you misconfigure Json.NET you may be vulnerable:
C#
var settings = new JsonSerializerSettings
{
// Limit the object graph we'll consume to a fixed depth. This prevents stackoverflow exceptions
// from deserialization errors that might occur from deeply nested objects.
MaxDepth = 32,
// Setting this to None prevents Json.NET from loading malicious, unsafe, or security-sensitive types
TypeNameHandling = TypeNameHandling.None,
};
JsonConvert.DeserializeObject<T>(jsonString, settings);
JavaScriptSerializer is also vulnerable if you do not set a type resolver:
C#
var resolver = new CustomJavaScriptTypeResolver();
var serializer = new System.Web.Script.Serialization.JavaScriptSerializer(resolver);
serializer.RecursionLimit = 32;
#More generally, how to prevent that attack?
- When you roundtrip state to a user-controlled space, sign the data. Thus, you can validate that the data has been generated by your application and it should not contain any malicious data.
- Don't publish the signing key, so no one can generate the malicious payload.
- Don't use any serialization format that allows an attacker to specify the object type to be deserialized. It may require some configuration for some serializers.
Do you have a question or a suggestion about this post? Contact me!