Ever since .NET Core 1.0, we in .NET Security have been trying to lay BinaryFormatter to rest. It’s long been known by security practitioners that any deserializer, binary or text, which allows its input to carry information about the objects to be created is a security problem waiting to happen. There is even a Common Weakness Enumeration (CWE) that describes the issue, CWE-502 “Deserialization of Untrusted Data” and examples of this type of vulnerability run from security issues in Exchange through security issues in Apache. Within Microsoft the use of BinaryFormatter with untrusted input has caused many instances of heartache, late nights, and weekend work trying to produce a solution.
In .NET Core 1.0 we removed BinaryFormatter entirely due to its known risks, but without a clear path to using something safer customer demand brought it back to .NET Core 1.1. Since then, we have been on the path to removal, slowly turning it off by default in multiple project types but letting you opt-in via flags if you still needed it for backward compatibility. .NET 9 sees the culmination of this effort with the removal of BinaryFormatter. In .NET 9 these flags will no longer exist and the in-box implementation of BinaryFormatter will throw exceptions in any project type when you try to use it. However, if you are committed to using a class that cannot be made secure you will still be able to.
It's not that it’s binary
We pick on BinaryFormatter because of its ubiquitous use, but the problem is not that the payload is binary, the problem is that the payload tells .NET what objects to create. That capability is what makes BinaryFormatter so popular, you can throw pretty much any class at it, and it will serialize and deserialize it correctly without developers having to do any work. However, it is also what makes it so dangerous.
There are classes within .NET which, by design, can run arbitrary code, and if you try to deserialize those through any deserialization code that creates objects based on the payload you end up with “Remote Code Execution” (RCE), a class of security vulnerability that allows the attacker to run their own code within your application, under the privileges your application has.
Imagine your code has a class DeleteAllMyData. If you accept a payload from untrusted input, for example a web page, an attacker can create a payload that contains an instance of DeleteAllMyData and when you deserialize that payload you will get a chance to discover if your database backups actually restore.
Other .NET serialization formats (and serialization formats in other languages and frameworks) allow for class information to be passed in a payload, to pick on our own framework SoapFormatter, LosFormatter, NetDataContractSerializer and ObjectStateFormatter all exhibit the same dangerous characteristics.
Even JSON.NET has an opt-in option to allow the creation of a payload that specifies class names, and that is a text format, not a binary one. Other JSON deserializers (both .NET and Java) suffered from the same problem, as demonstrated at Black Hat 2017, Friday the 13th JSON Attacks, Alvaro Muñoz & Oleksandr Mirosh.
Your takeaway should be that there’s nothing inherently wrong with a binary payload, it’s the fact that the BinaryFormatter payload contains instructions on the classes to be created, and what should go into that class’s properties that make it so dangerous. Other serializers, including some JSON serializers, also have the same ability, making them equally dangerous to use.
Real Word Examples of BinaryFormatter security bugs
Within Microsoft, BinaryFormatter has been the cause of a quite a few security problems, including:
In Exchange, CVE-2021-42321, where a binder had a spelling mistake that led to RCE.
In Azure DevOps, CVE-2019-1306, where invalid MarkDown used BinaryFormatter and granted RCE.
In SharePoint, CVE-2022-22005, where loading images for charts used BinaryFormatter and once again lead to RCE.
There has been an outright ban on using BinaryFormatter for new projects at Microsoft for years, and an ongoing effort to remove it from older code for almost as long as the usage ban in new code. We have added code analyzers, warnings, and public documentation, and yet still we see people using it and ending up with RCE bugs. It feels like we need to do more.
BinaryFormatter is not fixable
BinaryFormatter has other problems around algorithmic complexity, unbounded cache growth, and unintended object graph edge manipulation. If we take out one insecure piece, replace it with a secure piece, and keep going until all the insecure pieces are replaced there is not much of the original BinaryFormatter left. A secure version would have a vastly different API shape and it could not be a drop-in replacement. It would require massive refactoring of application code to use, so, really, would it even be BinaryFormatter anymore?
Serialization binders, a type of class used to inspect types during deserialization, are often proposed as a control mechanism to stop BinaryFormatter doing bad things, but there are code paths in BinaryFormatter that bypass binders by design, and writing a correct binder that isn’t vulnerable to a Denial of Service (DoS) attack is extremely hard.
The plan for .NET 9
For the last few .NET releases newly introduced project types such as Blazor or MAUI projects have been unable to use BinaryFormatter at all. ASP.NET projects are particularly vulnerable as their entire purpose is to take untrusted input and were also part of the ban. For some, older, project types you have been able to opt into using BinaryFormatter by setting a flag.
Our current intentions are:
The in-box implementation of BinaryFormatter will throw exceptions when you try to use it, even if you set any of the settings that previously enabled its use. We are also removing these settings.
A new NuGet package will be provided, (with a suitable name like Microsoft.Unsupported.BinaryFormatter) which you could add to your projects. With a small config change in your app, BinaryFormatter will appear again, which is useful if you judge the risk of BinaryFormatter acceptable for your use cases. This NuGet package will be permanently marked as vulnerable so that dependency and code scanners can detect BinaryFormatter usage easily.
We will also provide a safe reader in its own NuGet package. This reader will allow you to read a BinaryFormatter payload and it will return an object graph describing the payload. Each node in the graph will represent an object in the payload and each edge represents a reference between objects.
The safe reader will allow you to evaluate the classes within a BinaryFormatter payload, providing a representation of the object graph with each node containing information on the object type, and a list of its fields and their values. The node structure we are considering looks as follows:
public class Node
{
public SomeTypeDescriptor ObjectType;
public Dictionary<string, object> Fields;
}
We will use a new class for the object type descriptor (whose name is yet to be decided) to impede naïve developers calling Type.GetType(theNodeType) and reintroducing the same fundamentally insecure behavior that we are trying to stop.
Say you had had a class Customer and a class Address, and your customer had a ShippingAddress and BillingAddress, both of which are properties in the customer class, with a type of Address. The fields within the Customer object node would be something like { "ShippingAddress", node_for_ship_addr } and { "BillingAddress", node_for_bill_addr }.
This graph will allow you to both validate the types in the payload against types you are expecting and provide data you can use to rehydrate your objects.
Preparing for migration
To migrate away from BinaryFormatter is going to take work. There is no drop-in replacement, nor is there a single path that fits everyone because everyone’s classes are different and there are too many varied use cases.
The first step to migrating away from BinaryFormatter should be selecting a new serialization format. Remember that binary representation is safe if the produced payload does not instruct the deserialization process to create types. The same restriction applies to text-based formats.
At Microsoft, a lot of teams have moved from BinaryFormatter to [ProtoBuf](https://protobuf.dev/getting-started/csharptutorial/) (you will even see some familiar names in the commit history for the C# version). Other teams, whose data is text and where textual representation have advantages like easy searching have moved to JSON.
There are a lot of choices out there, but we cannot pick a winner for you. Each potential serializer has pluses and minuses, and how you weigh these is a decision only you can make for your application. Factors can include speed, memory consumption, storage costs, whether it is human readable, whether it is standards based and so on.
The ideal safe use of the proposed SafeReader class is to perform conversions of existing data into your chosen new serialization format and run this conversion in as much isolation as possible, for example a virtual machine or a container, again analyzing the types in each payload, rehydrating objects, and then reserializing them in your chosen serialization format.
If your class properties are primitive types, for example int, bool, or strings then the proposed safe reader could work as part of a replacement deserialization mechanism at runtime, leaving you to do the object rehydration once you have validated the types are those you expect.
Or, if you accept the risk of Remote Code Execution, or, if you are sure that no-one can tamper with your messages or data (such a promise is extremely hard to make for online systems or for desktop or mobile apps) then you might consider using the proposed NuGet package. If you choose to take this route, please remember that this package is unsupported. We will not fix security issues in BinaryFormatter.
Summary
If you are using BinaryFormatter now you should start looking at alternative serialization formats and decide what works for you in your specific circumstance, be it saved object graphs, network transports, temporary round tripped data, or any other type of data.
You should look at all your saved data and plan to convert any data serialized by BinaryFormatter into the new serialization format you have chosen.
You should evaluate how much backwards compatibility you need, and how long you need it for.
Finally, you need to get off BinaryFormatter, it is too dangerous to use any more and that is why it is leaving the .NET runtime.
Ever since .NET Core 1.0, we in .NET Security have been trying to lay
BinaryFormatter
to rest. It’s long been known by security practitioners that any deserializer, binary or text, which allows its input to carry information about the objects to be created is a security problem waiting to happen. There is even a Common Weakness Enumeration (CWE) that describes the issue, CWE-502 “Deserialization of Untrusted Data” and examples of this type of vulnerability run from security issues in Exchange through security issues in Apache. Within Microsoft the use ofBinaryFormatter
with untrusted input has caused many instances of heartache, late nights, and weekend work trying to produce a solution.In .NET Core 1.0 we removed
BinaryFormatter
entirely due to its known risks, but without a clear path to using something safer customer demand brought it back to .NET Core 1.1. Since then, we have been on the path to removal, slowly turning it off by default in multiple project types but letting you opt-in via flags if you still needed it for backward compatibility. .NET 9 sees the culmination of this effort with the removal ofBinaryFormatter
. In .NET 9 these flags will no longer exist and the in-box implementation ofBinaryFormatter
will throw exceptions in any project type when you try to use it. However, if you are committed to using a class that cannot be made secure you will still be able to.It's not that it’s binary
We pick on
BinaryFormatter
because of its ubiquitous use, but the problem is not that the payload is binary, the problem is that the payload tells .NET what objects to create. That capability is what makesBinaryFormatter
so popular, you can throw pretty much any class at it, and it will serialize and deserialize it correctly without developers having to do any work. However, it is also what makes it so dangerous.There are classes within .NET which, by design, can run arbitrary code, and if you try to deserialize those through any deserialization code that creates objects based on the payload you end up with “Remote Code Execution” (RCE), a class of security vulnerability that allows the attacker to run their own code within your application, under the privileges your application has.
This has been a known problem as far back as 2012, when the Black Hat security conference featured the canonical presentation on .NET’s version of the problem, Are you my type? Breaking .NET through Serialization, James Forshaw. There’s even a GitHub repository dedicated to hunting and showing these types of attacks in .NET, https://github.com/pwntester/ysoserial.net.
Imagine your code has a class DeleteAllMyData. If you accept a payload from untrusted input, for example a web page, an attacker can create a payload that contains an instance of DeleteAllMyData and when you deserialize that payload you will get a chance to discover if your database backups actually restore.
Other .NET serialization formats (and serialization formats in other languages and frameworks) allow for class information to be passed in a payload, to pick on our own framework
SoapFormatter
,LosFormatter
,NetDataContractSerializer
andObjectStateFormatter
all exhibit the same dangerous characteristics.Even JSON.NET has an opt-in option to allow the creation of a payload that specifies class names, and that is a text format, not a binary one. Other JSON deserializers (both .NET and Java) suffered from the same problem, as demonstrated at Black Hat 2017, Friday the 13th JSON Attacks, Alvaro Muñoz & Oleksandr Mirosh.
Your takeaway should be that there’s nothing inherently wrong with a binary payload, it’s the fact that the
BinaryFormatter
payload contains instructions on the classes to be created, and what should go into that class’s properties that make it so dangerous. Other serializers, including some JSON serializers, also have the same ability, making them equally dangerous to use.Real Word Examples of BinaryFormatter security bugs
Within Microsoft,
BinaryFormatter
has been the cause of a quite a few security problems, including:BinaryFormatter
and granted RCE.BinaryFormatter
and once again lead to RCE.There has been an outright ban on using
BinaryFormatter
for new projects at Microsoft for years, and an ongoing effort to remove it from older code for almost as long as the usage ban in new code. We have added code analyzers, warnings, and public documentation, and yet still we see people using it and ending up with RCE bugs. It feels like we need to do more.BinaryFormatter is not fixable
BinaryFormatter
has other problems around algorithmic complexity, unbounded cache growth, and unintended object graph edge manipulation. If we take out one insecure piece, replace it with a secure piece, and keep going until all the insecure pieces are replaced there is not much of the originalBinaryFormatter
left. A secure version would have a vastly different API shape and it could not be a drop-in replacement. It would require massive refactoring of application code to use, so, really, would it even beBinaryFormatter
anymore?Serialization binders, a type of class used to inspect types during deserialization, are often proposed as a control mechanism to stop
BinaryFormatter
doing bad things, but there are code paths inBinaryFormatter
that bypass binders by design, and writing a correct binder that isn’t vulnerable to a Denial of Service (DoS) attack is extremely hard.The plan for .NET 9
For the last few .NET releases newly introduced project types such as Blazor or MAUI projects have been unable to use
BinaryFormatter
at all. ASP.NET projects are particularly vulnerable as their entire purpose is to take untrusted input and were also part of the ban. For some, older, project types you have been able to opt into usingBinaryFormatter
by setting a flag.Our current intentions are:
BinaryFormatter
will throw exceptions when you try to use it, even if you set any of the settings that previously enabled its use. We are also removing these settings.BinaryFormatter
will appear again, which is useful if you judge the risk ofBinaryFormatter
acceptable for your use cases. This NuGet package will be permanently marked as vulnerable so that dependency and code scanners can detectBinaryFormatter
usage easily.BinaryFormatter
payload and it will return an object graph describing the payload. Each node in the graph will represent an object in the payload and each edge represents a reference between objects.The safe reader will allow you to evaluate the classes within a
BinaryFormatter
payload, providing a representation of the object graph with each node containing information on the object type, and a list of its fields and their values. The node structure we are considering looks as follows:We will use a new class for the object type descriptor (whose name is yet to be decided) to impede naïve developers calling
Type.GetType(theNodeType)
and reintroducing the same fundamentally insecure behavior that we are trying to stop.Say you had had a class
Customer
and a classAddress
, and your customer had aShippingAddress
andBillingAddress
, both of which are properties in the customer class, with a type ofAddress
. The fields within theCustomer
object node would be something like{ "ShippingAddress", node_for_ship_addr }
and{ "BillingAddress", node_for_bill_addr }
.This graph will allow you to both validate the types in the payload against types you are expecting and provide data you can use to rehydrate your objects.
Preparing for migration
To migrate away from
BinaryFormatter
is going to take work. There is no drop-in replacement, nor is there a single path that fits everyone because everyone’s classes are different and there are too many varied use cases.The first step to migrating away from
BinaryFormatter
should be selecting a new serialization format. Remember that binary representation is safe if the produced payload does not instruct the deserialization process to create types. The same restriction applies to text-based formats.At Microsoft, a lot of teams have moved from
BinaryFormatter
to[ProtoBuf](https://protobuf.dev/getting-started/csharptutorial/)
(you will even see some familiar names in the commit history for the C# version). Other teams, whose data is text and where textual representation have advantages like easy searching have moved to JSON.There are a lot of choices out there, but we cannot pick a winner for you. Each potential serializer has pluses and minuses, and how you weigh these is a decision only you can make for your application. Factors can include speed, memory consumption, storage costs, whether it is human readable, whether it is standards based and so on.
The ideal safe use of the proposed SafeReader class is to perform conversions of existing data into your chosen new serialization format and run this conversion in as much isolation as possible, for example a virtual machine or a container, again analyzing the types in each payload, rehydrating objects, and then reserializing them in your chosen serialization format.
If your class properties are primitive types, for example int, bool, or strings then the proposed safe reader could work as part of a replacement deserialization mechanism at runtime, leaving you to do the object rehydration once you have validated the types are those you expect.
Or, if you accept the risk of Remote Code Execution, or, if you are sure that no-one can tamper with your messages or data (such a promise is extremely hard to make for online systems or for desktop or mobile apps) then you might consider using the proposed NuGet package. If you choose to take this route, please remember that this package is unsupported. We will not fix security issues in
BinaryFormatter
.Summary
If you are using
BinaryFormatter
now you should start looking at alternative serialization formats and decide what works for you in your specific circumstance, be it saved object graphs, network transports, temporary round tripped data, or any other type of data. You should look at all your saved data and plan to convert any data serialized byBinaryFormatter
into the new serialization format you have chosen.You should evaluate how much backwards compatibility you need, and how long you need it for.
Finally, you need to get off
BinaryFormatter
, it is too dangerous to use any more and that is why it is leaving the .NET runtime.Discussion Issue