Jackson polymorphic deserialization explained

Jackson polymorphic deserialization explained

Written by

in

Jackson polymorphic deserialization is a Jackson feature that lets a JSON document decide which Java class it should become. That is convenient when your code stores mixed types. It is also how a plain looking JSON body turns into a gadget trigger, because the person sending the JSON, not your code, gets to pick the class that Jackson builds.

What jackson polymorphic deserialization does

Jackson maps JSON to Java objects. Normally you hand it a target type and it fills in the fields. Polymorphic typing changes the deal. When a field is declared as an interface or a base class, Jackson has to decide which subclass to create. So it reads a type hint straight out of the JSON and builds that type.

There are two common ways this gets switched on. The blunt one is default typing, set once on the mapper so it applies everywhere:

ObjectMapper mapper = new ObjectMapper();
mapper.activateDefaultTyping(
    LaissezFaireSubTypeValidator.instance,
    ObjectMapper.DefaultTyping.NON_FINAL);

The narrower one is an annotation on a field or a type:

@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
private Object payload;

Both do the same risky thing. They let the incoming document carry a class name, and they trust it.

The type hint sits in the JSON

With default typing on, Jackson reads a two element array where the first element is the class name and the second is the object:

["com.acme.notes.Attachment", {"name": "report.pdf", "size": 2048}]

That first string is an instruction. Jackson loads the named class, then deserializes the object into it. If a user controls the request body, the user controls that string, and now the user is choosing which class your server constructs.

Once the JSON picks the class, you are not parsing data any more. You are letting a stranger name the objects your program builds.

How a type hint becomes a gadget trigger

Picking the class is only step one. The damage comes from which classes are reachable. An attacker does not send your Attachment class. They swap the name for a class that is already on the application classpath and that does something useful while it is being built or configured, such as opening a network connection or loading code from a remote source.

Jackson builds the object by calling a constructor and then setter methods for the fields in the JSON. If a class on the classpath has a setter that, for example, connects to a database URL you supply, then simply deserializing a crafted object makes that call happen. Stitch a few of these together and the act of parsing JSON ends in remote code execution. That stitched sequence is the core idea behind a deserialization gadget chain, and the automatic methods it abuses are the same ones described in magic methods in deserialization attacks. No working chain is shown here, on purpose. The defense does not depend on the exact classes.

The key point is that the attacker uploads no new code. They arrange classes your app already ships so that rebuilding one object runs them in an order nobody intended. If you want the first principles version, start with the insecure deserialization primer.

Spotting it in a codebase

  • Search for default typing. Look for activateDefaultTyping, the older enableDefaultTyping, and any mapper that enables typing globally. This is the highest risk setting because it applies to every Object field.
  • Find the annotations. Grep for @JsonTypeInfo with Id.CLASS or Id.MINIMAL_CLASS. These embed a full class name in the JSON and let it be chosen at parse time.
  • Trace the source of the JSON. A typed mapper reading a config file you ship is fine. The same mapper reading a request body, a message from a queue, or a cookie is the risk. Follow the bytes back to where they enter.
  • Look for fields typed as Object or a broad interface. Those are the slots where Jackson has to ask the JSON which class to build.

How to fix jackson polymorphic deserialization safely

The clean fix is to stop letting the document choose the class.

  • Turn default typing off. Do not call activateDefaultTyping on a mapper that reads untrusted input. If you never needed polymorphism, you never needed the risk.
  • Deserialize into concrete types. Map the body into a specific class you define, with named fields, not into Object. Then the type is fixed by your code, not by the JSON.
  • If you truly need polymorphism, use a strict allowlist. Jackson supports a PolymorphicTypeValidator that only permits a named set of subtypes. Name the handful of classes you expect and reject everything else by default.
PolymorphicTypeValidator ptv = BasicPolymorphicTypeValidator.builder()
    .allowIfSubType("com.acme.notes.Attachment")
    .allowIfSubType("com.acme.notes.Comment")
    .build();
ObjectMapper mapper = JsonMapper.builder()
    .polymorphicTypeValidator(ptv)
    .build();
  • Prefer a name to class mapping you control. Use @JsonSubTypes with short logical names, so the JSON carries a tag like "kind": "attachment" instead of a raw class name. The client never gets to name a class at all.
  • Keep dependencies current. Many known gadget classes live in old library versions. Patching removes classes an attacker would reach for.

Why this bug rewards reading the app

You do not find this by firing a fixed payload list. You find it by noticing that a mapper has default typing on, then asking whether untrusted bytes can reach it, and which classes sit on the classpath to be chosen. That is assumption testing, not pattern matching. See the injection and input category for the family this belongs to, and the related fastjson autoType and TypeNameHandling writeups for the same idea in other libraries.

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

Frequently asked questions

What is Jackson polymorphic deserialization?

It is a Jackson feature where a JSON document carries a type hint, such as a class name in a two element array, and Jackson builds that class instead of a fixed type. It is enabled by default typing on the mapper or by the @JsonTypeInfo annotation. When the JSON is attacker controlled, the attacker chooses which class your server constructs.

Why is default typing in Jackson dangerous?

Default typing applies to every Object or base class field, so any untrusted JSON can name a class for Jackson to build. If a class on the application classpath does something useful while being constructed, such as opening a connection, an attacker can reach it and start a gadget chain that can end in remote code execution.

How do I disable Jackson polymorphic deserialization safely?

Do not call activateDefaultTyping on a mapper that reads untrusted input, and map request bodies into concrete classes you define. If you truly need polymorphism, configure a PolymorphicTypeValidator that allows only a named set of subtypes and rejects everything else by default.

Is an allowlist better than blocking known gadget classes?

Yes. A blocklist of bad classes never stays complete, because attackers keep finding new classes the deserializer can reach. An allowlist names the small set of classes you expect and denies the rest, so it holds even as new gadget classes appear.


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.