Insecure deserialization is one of the few application vulnerabilities that reliably yields remote code execution from a single crafted input, with no memory-corruption bug required. In .NET, the mechanism is a formatter that reconstructs arbitrary object types from data — and an attacker who controls that data controls which types get built, wiring together "gadget" classes whose construction or property-setting triggers dangerous behavior. Microsoft has spent years walling this off, culminating in BinaryFormatter's removal from the runtime in .NET 9. But the risk lives on in JSON settings, legacy types, and third-party libraries. This post explains how the attack works and how to eliminate it from a .NET codebase.
How deserialization becomes remote code execution
Serialization turns an object into bytes; deserialization turns bytes back into objects. The danger is type-aware deserialization: when the serialized data itself specifies which .NET type to instantiate, an attacker can name a "gadget" type whose deserialization does something useful to them. Chains of these gadgets — well documented in tooling like ysoserial.net — can escalate a single Deserialize call into command execution, file writes, or SSRF. The vulnerable pattern is always the same shape: untrusted bytes flow into a deserializer that will happily build whatever types those bytes request.
// The classic RCE primitive: deserializing attacker-controlled bytes
var stream = new MemoryStream(Convert.FromBase64String(userInput));
var formatter = new BinaryFormatter();
var obj = formatter.Deserialize(stream); // reconstructs arbitrary types
What changed: BinaryFormatter is gone in .NET 9
Microsoft deprecated BinaryFormatter progressively: an obsoletion warning (SYSLIB0011), then disabled-by-default behavior, and in .NET 9 the implementation was removed from the runtime. Calling it now throws unless you pull an out-of-support compatibility package — which you should not do. The same deprecation applies to the related legacy formatters SoapFormatter and the NetDataContractSerializer. If your codebase or a dependency still relies on BinaryFormatter, treat migrating off it as a security task, not just a compatibility one. LosFormatter and ObjectStateFormatter (ASP.NET view-state related) share the same lineage and the same danger.
The risk that's still everywhere: JSON.NET TypeNameHandling
Most teams don't call BinaryFormatter anymore — but many use Newtonsoft.Json with type-name handling turned on, which reintroduces the exact same RCE class through JSON:
// DANGEROUS: the JSON names the CLR type to instantiate
var settings = new JsonSerializerSettings {
TypeNameHandling = TypeNameHandling.All
};
var obj = JsonConvert.DeserializeObject<BaseType>(userJson, settings);
TypeNameHandling.All, .Auto, and .Objects cause Newtonsoft to read a $type field from the JSON and instantiate whatever type it names — so attacker-controlled JSON can request a gadget type, exactly like BinaryFormatter does with binary data. Microsoft's own documentation warns against enabling this with untrusted input. The fixes, in order of preference:
- Don't set
TypeNameHandling— the default (None) is safe. - If you truly need polymorphism, provide a custom
SerializationBinderthat allow-lists a small, explicit set of permitted types. - Migrate to
System.Text.Json, the ASP.NET Core default, which does not resolve arbitrary types by name. For polymorphism it uses opt-in[JsonDerivedType]attributes with an explicit discriminator — you declare the allowed subtypes at compile time rather than letting the payload choose.
// System.Text.Json: safe, explicit polymorphism
[JsonDerivedType(typeof(CardPayment), "card")]
[JsonDerivedType(typeof(BankPayment), "bank")]
public abstract class Payment { }
The overlooked one: DataSet and DataTable from XML
DataSet and DataTable can deserialize XML in a way that also allows type instantiation, which is why they've been the subject of .NET security advisories in the past. Don't populate a DataSet/DataTable by reading XML from an untrusted source, and be wary of any serializer configured to handle these types from external input. The same caution applies to XmlSerializer when its type set is driven by input, and to LosFormatter-backed view state that isn't integrity-protected.
Safe deserialization principles
The defenses generalize beyond any single API:
| Principle | In practice |
|---|---|
| Prefer data-only formats | System.Text.Json, Protocol Buffers, MessagePack without type embedding |
| Never deserialize types from untrusted input | No TypeNameHandling, no BinaryFormatter |
| Allow-list when polymorphism is unavoidable | SerializationBinder or [JsonDerivedType] discriminators |
| Validate after deserializing | Range/format checks on the resulting object |
| Integrity-protect serialized state | Sign/HMAC any client-held serialized data (e.g., tokens, view state) |
| Least privilege at runtime | A compromised deserializer can't do what the process can't do |
An insecure-deserialization checklist
- No
BinaryFormatter,SoapFormatter,NetDataContractSerializer,LosFormatter -
Newtonsoft.JsonTypeNameHandlingisNone(or an allow-list binder) -
System.Text.Jsonused with attribute-based polymorphism only - No
DataSet/DataTablepopulated from untrusted XML - Client-held serialized state is signed/HMAC-protected
- Objects validated after deserialization, before use
Because a vulnerable deserializer can hide in a transitive dependency you never call directly, pairing code review with software composition analysis of your NuGet tree is what makes coverage complete.
How Safeguard Helps
Insecure deserialization is a needle-in-a-haystack problem — one dangerous setting or one legacy formatter, anywhere in your code or your dependencies, is enough. Safeguard scans for it on both fronts. Its static analysis flags BinaryFormatter usage, TypeNameHandling set to a dangerous value, and unsafe DataSet/DataTable XML loading in your C# code, while software composition analysis catches vulnerable deserialization gadgets and outdated serializers pulled in transitively through NuGet. Griffin AI traces whether untrusted input can actually reach the deserializer and explains the safe rewrite, so your team fixes exploitable paths rather than every theoretical one, and dynamic testing confirms exploitability against the running service. For real findings, the auto-fix workflow proposes the migration — removing TypeNameHandling or moving to System.Text.Json — as a reviewable pull request. Teams evaluating deep taint analysis often compare it on the Safeguard vs Checkmarx page.
Start free at app.safeguard.sh/register and read the deserialization rule details at docs.safeguard.sh.