DEV Community

Java JSON deserialization problems with the Jackson ObjectMapper

In a previous blog post, we took a look at Java's custom serialization platform and what the security implications are. And more recently, I wrote about how improvements in Java 17 can help you prevent insecure deserialization. However, nowadays, people aren't as dependent on Java's custom serialization, opting instead to use JSON. JSON is the most widespread format for data serialization, it is human readable and not specific to Java.

One of the most commonly used libraries is jackson-databind, which provides you with an ObjectMapper to transform your object into JSON and vice versa.

As it is a popular library, it's very important to know that the jackson- databind library has been subject to many reported vulnerabilities over the past couple of years. Nevertheless, this does not mean that using the Jackson ObjectMapper is a security risk by default. This article will explain how the Jackson deserialization vulnerabilities work and how to make sure you are not affected by them.

Using the Jackson ObjectMapper to create Java objects from JSON

With the code below, we can create an ObjectMapper and use it to recreate a Person from a JSON string that comes from a file.

ObjectMapper om = new ObjectMapper(); 
Person myvalue = om.readValue(Files.readAllBytes(Paths.get("person.json")), Person.class); 
System.out.println("name:"+myvalue.name+" \n"+"Age:"+myvalue.age);
Enter fullscreen mode Exit fullscreen mode

This is all straightforward, and nothing really fancy is going to happen. Without any annotations, the Jackson ObjectMapper uses reflection to do the POJO mapping. Because of the reflection, it works on all fields regardless of the access modifier. If there are getters and setters available, the Jackson ObjectMapper will use that to do the mapping.

Default typing in Jackson

Many of the vulnerabilities with the Jackson library for JSON serialization depend on default typing, which is not enabled by default . You need to enable this explicitly. This means in most cases the vulnerabilities named in this list do not affect your system directly.

But let's explain what default typing is and what it is used for.

Default typing is a mechanism in the Jackson ObjectMapper to deal with polymorphic types and inheritance. If you want to deserialize JSON to a Java POJO but are unsure what subtype the object or the field is, you can simply deserialize to the superclass.

Say you have Coffee and Tea. Both classes have the same superclass of HotDrink. So if your Breakfast contains a HotDrink but you are unaware if it is either Coffee or Tea you can use default typing to solve this.

public class Breakfast {
   public String food;
   public HotDrink drink;
}

public abstract class HotDrink {
   public String name;
}

public class Coffee extends HotDrink {
   @Override
   public String toString() {
       return String.format("Coffee{name='%s'}", name);
   }
}


public class Tea extends HotDrink {
   @Override
   public String toString() {
       return String.format("Tea{name='%s'}", name);
   }
}

String breakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Tea",{"name":"oolong"}]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();

var myBreakfast = om.readValue(breakfastJson, Breakfast.class);
System.out.println("breakfast hotdrink:"+myBreakfast.drink);
Enter fullscreen mode Exit fullscreen mode

In this example, I enabled default typing on the ObjectMapper so that I can handle polymorphism everywhere I use this ObjectMapper. You can also do this on a specific field using the @JsonTypeInfo annotation.

Security problems with default typing on the Jackson ObjectMapper

So if default typing is enabled globally, it is possible to take inheritance to the extreme. If your Breakfast does not contain a HotDrink but a field of type Object, then any object can be available on the classpath. This also means we can deserialize any object that is available on the classpath. Potentially, this can be a gadget object that sets up a gadget chain and eventually ends in a remote execution.

These gadget chains are pretty similar to those described in my Serialization and deserialization in Java blog post. Let's simplify this with a single gadget that executes a command right away when initialized. My SecondBreakfast class blow, containing a drink of type Object.

public class Gadget {

   private Runnable command;

   public Gadget(String value) {
       this.command = new Command(value);
       this.command.run();
   }
}

public class SecondBreakfast {
   public String food;
   public Object drink;
}
Enter fullscreen mode Exit fullscreen mode

If I deserialize my SecondBreakfast with default typing enabled, I can deserialize an Object containing an arbitrary code execution.

String secondBreakfastJson = """
       {
           "food":"sandwich",
           "drink":["nl.brianvermeer.example.jackson.serialization.Gadget", "rm -rf *"]
       }
       """;

var om = new ObjectMapper();
om.enableDefaultTyping();



Var mySecondBreakfast = om.readValue(secondBreakfastJson, SecondBreakfast.class);
System.out.println("Second breakfast hotdrink:"+mySecondBreakfast.drink);
Enter fullscreen mode Exit fullscreen mode

Now this is a simplified example. But while finding and creating a gadget chain is not easy, it definitely is possible. Because of all the libraries and frameworks we use, chances are that a combination of all the classes in your classpath can be used to create such a gadget chain.

Also, there is already a large set of well-known "nasty classes" identified. Deserialization of such a class is considered dangerous and basically follows the pattern as described above. For reference, check the list of deserialization vulnerabilities on the jackson-databind library on the Snyk vulnerability database

How does this impact my application?

Ok, don't panic because it is not that bad. First of all, the maintainers of the jackson-databind library actively block the set of "nasty classes" in the SubTypeValidator. Secondly, you need to enable default types explicitly. This means by default, this setting is off, and polymorphic deserialization is not even possible.

Scanning with SCA tools like Snyk does show the vulnerability in the scan result. A quick search shows that there were a lot of these problems in the past. It is always wise to update to the newest version because the library is well maintained, and new "nasty classes" will be actively blocked when found. Nevertheless, the best way is to prevent enabling default typing of your Jackson ObjectMapper.

Snyk triage assistant to the rescue

Snyk is currently making an effort to filter these vulnerabilities if your code does not meet the prerequisites. For the Jackson ObjectMapper this means, if your code does not enable polymorphic typing, we will show you that it is unlikely that this specific vuln will exploit you. At the time of writing, this feature is only released for the Jackson deserialization vulnerability, but the team continues working on improving and expanding this feature. To use this feature, we need approval to scan your code to check if you do not have default typing enabled.

The code examples used in this blog post are published on my GitHub account. Feel free to fork or reuse these examples in any way you prefer.

Deserialize safely!

The best way to avoid deserialization problems with the Jackson ObjectMapper is to prevent polymorphic typing. Please do not enable default typing for your ObjectMapper. Also, connect your project to Snyk to find out if you are using a jackson-databind library with known vulnerabilities. In most cases, you can easily replace it with a newer version. When you enable code scanning, the Triage Assistant can help you determine if it is likely or unlikely to be exploitable.

Latest comments (0)