Skip to main content

Java JSON deserialization problems with the Jackson ObjectMapper

Escrito por:
wordpress-sync/Blog-Feature-Java-deserialize

1 de dezembro de 2021

0 minutos de leitura

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.

But before we get too far, let's answer a few questions you may have...

What is deserialization?

Deserialization is transforming the data from a file or stream back into an object to be used in your application. This can be binary data or structured data like JSON and XML. Deserialization is the opposite of serialization, which transforms objects into byte streams or structured text.

What is the Jackson ObjectMapper used for?

The Jackson Objectmapper is part of the Jackson databind library and is used the transform JSON into Java objects and vice versa. It is the most commonly used and well-known libraries in the Java ecosystem to convert JSON to and from Java, and it is automatically shipped with Spring Boot.

Is the Jackson Objectmapper secure?

Yes, the Jackson Objectmapper is secure and uses secure defaults. It is essential that you update to the most recent versions to avoid known security issues.

Is creating an ObjectMapper expensive?

Creating an ObjectMapper is quite expensive. Therefore it is recommended that you reuse your ObjectMapper instance. The Jackon ObjectMapper is thread-safe so it can safely be reused.

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.

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

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.

1public class Breakfast {
2   public String food;
3   public HotDrink drink;
4}
5
6public abstract class HotDrink {
7   public String name;
8}
9
10public class Coffee extends HotDrink {
11   @Override
12   public String toString() {
13       return String.format("Coffee{name='%s'}", name);
14   }
15}
16
17public class Tea extends HotDrink {
18   @Override
19   public String toString() {
20       return String.format("Tea{name='%s'}", name);
21   }
22}
23
24String breakfastJson = """
25       {
26           "food":"sandwich",
27           "drink":["nl.brianvermeer.example.jackson.serialization.Tea",{"name":"oolong"}]
28       }
29       """;
30
31var om = new ObjectMapper();
32om.enableDefaultTyping();
33
34var myBreakfast = om.readValue(breakfastJson, Breakfast.class);
35System.out.println("breakfast hotdrink:"+myBreakfast.drink);

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.

1public class Gadget {
2
3   private Runnable command;
4
5   public Gadget(String value) {
6       this.command = new Command(value);
7       this.command.run();
8   }
9}
10
11public class SecondBreakfast {
12   public String food;
13   public Object drink;
14}

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

1String secondBreakfastJson = """
2       {
3           "food":"sandwich",
4           "drink":["nl.brianvermeer.example.jackson.serialization.Gadget", "rm -rf *"]
5       }
6       """;
7
8var om = new ObjectMapper();
9om.enableDefaultTyping();
10
11Var mySecondBreakfast = om.readValue(secondBreakfastJson, SecondBreakfast.class);
12System.out.println("Second breakfast hotdrink:"+mySecondBreakfast.drink);

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.

wordpress-sync/blog-jackson-objectmapper-snyk

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.