TypeNameHandling in Json.NET and RCE risk

TypeNameHandling in Json.NET and RCE risk

Written by

in

TypeNameHandling is a setting in Json.NET, the widely used .NET JSON library also known as Newtonsoft.Json, that writes and reads a type name inside the JSON. When it is set to All or Auto, the JSON carries a $type field that tells the parser which .NET class to build. If the JSON comes from a user, that setting lets the user choose the class your server constructs, which is the starting point for a gadget in .NET.

What TypeNameHandling does

Json.NET turns JSON into .NET objects. By default it uses the type you ask for. TypeNameHandling changes that. When a property is an interface, an object, or a base class, the serializer needs a way to record the real type so it can rebuild it later. So it embeds the full .NET type name and assembly in a $type field.

{
  "$type": "Acme.Notes.Attachment, Acme.Notes",
  "Name": "report.pdf",
  "Size": 2048
}

The values that matter:

  • TypeNameHandling.None is the safe default. No $type is written or read.
  • TypeNameHandling.All writes $type on every object and reads it back.
  • TypeNameHandling.Auto writes it when the declared type and the real type differ, and reads whatever $type arrives.

With All or Auto, the $type string is an instruction. The parser loads that type and builds it. If the request body is attacker controlled, so is the type.

The moment the JSON names the class, the client is choosing what your process constructs. The $type field is not a label, it is a command.

How TypeNameHandling leads to a gadget in .NET

Choosing the type is only the opening move. Json.NET builds the object by running its constructor and setting its properties from the JSON. An attacker looks for a type already loaded in the application whose construction or property setters do something useful, such as starting a process, writing a file, or loading an assembly from a path they give. Point $type at that class, supply the property values, and the act of deserializing runs the behavior.

The attacker writes no new code. They name a class your app already carries and let Json.NET build it with values they choose. Because the assembly name rides along in the same field, the reach is as wide as everything loaded in the process, which is why a single typed endpoint can be enough.

The danger mirrors what BinaryFormatter does in the same runtime, covered in dotnet BinaryFormatter RCE. The sequence of reachable classes that ends in code execution is a deserialization gadget chain, and the underlying reason any of this is possible is in the insecure deserialization primer. No working chain appears here.

Spotting TypeNameHandling risk

  • Search for the setting. Grep for TypeNameHandling and flag any use of .All or .Auto. These are the values that read $type from input.
  • Check JsonSerializerSettings. The setting often sits in a shared settings object passed to JsonConvert.DeserializeObject. Follow that settings object to every call that uses it.
  • Trace the JSON source. Typed settings on internal, trusted data is one thing. The same settings on a request body, a cookie, or a message from a queue is the risk.
  • Watch for $type in traffic. Seeing that field arrive in user supplied JSON tells you the endpoint is type aware.

How to fix TypeNameHandling

  • Set TypeNameHandling.None on anything that reads untrusted JSON. This is the core fix. With None, the $type field is ignored and the client cannot name a class.
  • Deserialize into concrete types. Map the body into a specific class you define with named properties, not into object or a broad interface, so the type is fixed by your code.

If you genuinely need to round trip polymorphic types, do not accept raw type names. Bind the deserializer to a strict SerializationBinder that maps a short, known set of names to types and throws on anything else:

class AllowlistBinder : ISerializationBinder
{
    public Type BindToType(string assemblyName, string typeName)
    {
        if (typeName == "Attachment") return typeof(Acme.Notes.Attachment);
        if (typeName == "Comment")    return typeof(Acme.Notes.Comment);
        throw new JsonSerializationException("type not allowed");
    }
    public void BindToName(Type t, out string asm, out string name)
    { asm = null; name = t.Name; }
}
  • Default to deny. The binder says yes to a named list and throws on everything else. A blocklist of bad types does not hold, because new gadget types keep appearing.
  • Keep dependencies patched. Fewer old libraries on the runtime means fewer classes an attacker can reach through $type.

Why TypeNameHandling rewards understanding the app

A fixed payload list will not find this. The type that matters depends on what is loaded in that particular .NET process. You find it by noticing that a serializer reads $type, then asking whether untrusted input reaches it and which classes are present to be named. That is the same reasoning as ViewState deserialization elsewhere in the .NET stack. For the broader family, see the injection and input category.

This is the kind of flaw an autonomous researcher that reasons about whether untrusted input can reach a type resolving deserializer is built to find. Read how we think about it on our about page.

Frequently asked questions

What does TypeNameHandling do in Json.NET?

TypeNameHandling controls whether Json.NET writes and reads a $type field that names the .NET class to build. With All or Auto it reads that name from the JSON and constructs the type. With None, the default, $type is ignored.

Why are TypeNameHandling.All and Auto risky?

When the $type field is honored and the JSON is attacker controlled, the attacker chooses which .NET class the server builds. If a loaded type does something dangerous while constructing or setting properties, such as starting a process or loading an assembly, that becomes the start of a gadget chain.

What is the safe setting for TypeNameHandling?

Use TypeNameHandling.None on anything that reads untrusted JSON, and deserialize into concrete classes you define. If you must round trip polymorphic types, bind a strict SerializationBinder that maps a known set of names to types and throws on anything else.

Does a SerializationBinder fully fix the problem?

A binder helps only if it works as an allowlist, mapping a small set of expected names and throwing on the rest. A binder that tries to block known bad types will fall behind as new gadget types appear, so default to deny and name only what you expect.


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.