Why BinaryFormatter Deserialization Is Unsafe by Design

Why BinaryFormatter Deserialization Is Unsafe by Design

Written by

in

The binaryformatter deserialization problem is one of the clearest examples of a feature that is unsafe by design. BinaryFormatter.Deserialize takes a stream of bytes and rebuilds a full graph of .NET objects from it, including the types named inside that stream. If an attacker controls those bytes, they steer which types get created and how, and that is enough to run code on your server.

Why binaryformatter deserialization is dangerous

A serializer has one job: turn objects into bytes and back again. The trouble is that BinaryFormatter trusts the incoming bytes to tell it which types to build. The stream does not just carry data, it carries type names and field values. When the formatter reads the stream, it looks up each named type, allocates an instance, and fills in the fields, all before your own code sees the result.

This means the decision about what objects to construct is made by whoever wrote the bytes. For data your app produced and stored somewhere safe, that is fine. For data that arrived from a user, a queue, a cookie, or an upload, the sender now controls object construction inside your process.

The callbacks that turn data into execution

Reconstructing an object is not passive. The .NET serialization system fires several hooks as it rebuilds a graph, and each one runs real code on the attacker’s behalf:

  • A type can implement ISerializable and run logic in its deserialization constructor.
  • A type can implement IDeserializationCallback, whose OnDeserialization method runs once the graph is complete.
  • A type can carry an [OnDeserialized] method that the formatter calls automatically.
  • A type can implement IObjectReference, which lets the reconstructed object replace itself with a different object entirely.

None of these need your cooperation. They are part of the contract the formatter honors. So the attacker picks types that are already present in your app or its libraries, arranges their fields, and lets the callbacks fire. The types they pick are the links in a chain that ends at a method which executes a command, writes a file, or starts a process.

The attacker does not send you code. They send you a recipe made of classes you already trust, and the formatter follows it.

What a malicious payload looks like in shape

You do not need the exact bytes to understand the idea. Picture a vulnerable endpoint in an invented app called Acme Reports that accepts a saved filter as a base64 blob:

POST /reports/apply-filter
Content-Type: application/x-www-form-urlencoded

filter=AAEAAAD/////AQAAAAAAAAAM... (base64 of a serialized graph)

The server side does the unsafe step:

var bytes = Convert.FromBase64String(form["filter"]);
using var ms = new MemoryStream(bytes);
var formatter = new BinaryFormatter();
var filter = (ReportFilter)formatter.Deserialize(ms);  // attacker controls the graph

The cast to ReportFilter happens last. By the time that line runs, the formatter has already built every object in the stream and fired every callback. The cast failing does not save you, because the damage is done during Deserialize, not after it. This is why the fix is never to validate the result. You have to stop the loader from running on untrusted bytes in the first place. If you want the deeper mechanics of how those trusted classes get strung together, see what is a deserialization gadget chain, and for the class of bug in general, start with what is insecure deserialization.

The same pattern shows up in ViewState

BinaryFormatter style object graphs also ride inside other .NET features. The classic case is a misconfigured ViewState field that deserializes attacker supplied data with a weak or missing integrity key. The loader is different but the sink is the same: untrusted bytes become a reconstructed object graph. We cover that path on its own in dotnet viewstate deserialization. The gadget building technique, picking setter and callback behavior to reach a dangerous method, is the same idea described in pop chains and property oriented programming.

Why the platform deprecated it

Microsoft did not patch BinaryFormatter to make it safe, because there is no safe version of a feature whose whole purpose is to build arbitrary types from a stream. Instead the platform moved to turn it off. BinaryFormatter is marked obsolete, its use throws warnings you are told to treat as errors, and in recent .NET releases the methods are disabled by default and throw at run time unless you go out of your way to re enable them. The official guidance is blunt: do not call it on data you did not produce and protect yourself.

That deprecation is the strongest signal you will get from a platform. The right reading is not “find the flag to turn it back on.” It is “remove this and move to a serializer that does not reconstruct arbitrary types.”

The fix: a contract based serializer

The safe replacement is a serializer that only reads and writes the fields you declare, and never instantiates a type named by the input. These are often called contract based serializers because you, not the bytes, decide the shape.

  • Prefer System.Text.Json for most data. It maps JSON onto types you specify. It will not build a type just because a string in the payload asks for it, as long as you avoid polymorphic settings that reintroduce type names.
  • Use DataContractSerializer or XmlSerializer with a fixed set of known types when you need richer object models. You pass the allowed types in, so the input cannot name something else.
  • Do not swap in another format that embeds type names. The danger is type information in the payload, not the choice of binary versus text. A JSON serializer configured to honor a type hint field has the same problem.

Concretely, the earlier endpoint becomes:

var filter = JsonSerializer.Deserialize<ReportFilter>(form["filter"]);

Now the payload can only fill fields that exist on ReportFilter. There is no path for the input to pick a different type or trigger a serialization callback on a class you never meant to expose. If a value is out of range, you validate it like any other input. This is deserialization as data parsing, not as object construction, and that difference is the whole point. Object injection in other ecosystems works the same way, which is why it helps to read across languages, for example injection and input as a category.

A short checklist

  • Search your code and dependencies for BinaryFormatter, SoapFormatter, NetDataContractSerializer, and LosFormatter. Treat each hit as a finding, not a style note.
  • Confirm whether the input to each one can come from outside the app. If it can, it is a live risk.
  • Replace the call with a contract based serializer and a declared type. Validate the decoded object as normal data.
  • Make sure ViewState and any custom cookie or cache format are not quietly doing the same thing.

Finding this bug by hand means tracing untrusted bytes all the way to an unsafe loader, across a controller, a helper, and a cast that happens too late to matter. That source to sink reasoning over how an application actually wires its inputs is exactly what UnboundCompute is built to do. You can read more on our about page.

Frequently asked questions

Why is BinaryFormatter.Deserialize unsafe on untrusted data?

The byte stream it reads carries type names and field values, so the sender decides which .NET types get built and how. As the formatter reconstructs the object graph it fires callbacks like the deserialization constructor, OnDeserialization, and OnDeserialized, which run real code. An attacker strings together classes you already trust so those callbacks reach a method that executes a command.

Does casting the result to my expected type protect me?

No. The formatter builds every object in the stream and fires every callback during the Deserialize call. The cast to your expected type happens after that, so it runs too late to stop anything. The only safe fix is to keep the loader from running on untrusted bytes at all.

Why did Microsoft deprecate BinaryFormatter instead of fixing it?

There is no safe version of a feature whose purpose is to build arbitrary types named by the input. So the platform marked it obsolete, made its use raise errors, and disabled the methods by default in recent .NET releases so they throw at run time. The guidance is to remove it, not to re enable it.

What should I use instead of BinaryFormatter?

Use a contract based serializer that only reads the fields you declare and never instantiates a type named by the input. System.Text.Json works for most data, and DataContractSerializer or XmlSerializer with a fixed set of known types covers richer models. Avoid any configuration that honors a type hint in the payload, since that brings the same risk back.


Put an autonomous researcher on your own systems

UnboundCompute is an autonomous security researcher that reasons about how an application fits together and proves the access control and injection bugs it finds. We are opening a small number of founding design partner seats: private early access pointed at a staging target you choose, and a say in what it looks for. If your team ships software worth pressure testing, apply to the design partner program.