Safeguard
Security Guides

Insecure Deserialization in .NET: BinaryFormatter and Beyond

Why insecure deserialization is a remote-code-execution risk in .NET, what changed with BinaryFormatter's removal in .NET 9, and the dangerous JSON.NET settings still in the wild.

Marcus Chen
AppSec Engineer
5 min read

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 SerializationBinder that 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:

PrincipleIn practice
Prefer data-only formatsSystem.Text.Json, Protocol Buffers, MessagePack without type embedding
Never deserialize types from untrusted inputNo TypeNameHandling, no BinaryFormatter
Allow-list when polymorphism is unavoidableSerializationBinder or [JsonDerivedType] discriminators
Validate after deserializingRange/format checks on the resulting object
Integrity-protect serialized stateSign/HMAC any client-held serialized data (e.g., tokens, view state)
Least privilege at runtimeA compromised deserializer can't do what the process can't do

An insecure-deserialization checklist

  • No BinaryFormatter, SoapFormatter, NetDataContractSerializer, LosFormatter
  • Newtonsoft.Json TypeNameHandling is None (or an allow-list binder)
  • System.Text.Json used with attribute-based polymorphism only
  • No DataSet/DataTable populated 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.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.