Java JSON deserialization problems with the Jackson ObjectMapper
1. Dezember 2021
0 Min. LesezeitIn 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.
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.
Capture the Flag: Der Snyk Workshop
In unserem On-Demand Workshop erfahren Sie, wie Sie Capture the Flag Challenges erfolgreich abschließen.