Foojay Today

New Java 17 Features for Improved Security and Serialization

December 02, 2021

In December 2020, I wrote the article Serialization and deserialization in Java: explaining the Java deserialize vulnerability about the problems Java has with its custom serialization implementation. The serialization framework is so deeply embedded inside Java that knowing how dangerous some implementation can be is important. Insecure deserialization can lead to arbitrary code executions if a gadget chain is created from your classpath classes.

Recently, Java 17 — the new LTS version — was released. But how do the new features impact this problem, and can we prevent deserialization vulnerabilities better using these features?

In the blog post, I will look at three main Java 17 features:

  1. Records
  2. Java Flight Recorder (JFR) improvements
  3.  JEP 415 (Java Enhancement Proposal) Context-Specific Deserialization Filters.

1. Records

Records were introduced in Java 14 as a preview feature and became a fully released feature in Java 16. However, since many developers prefer to upgrade only to LTS versions, it makes sense to discuss Records in the context of serialization now Java 17 is fully released.

In contrast to normal POJOs when deserializing a Record, the constructor is used to recreate the object. For ordinary Java objects, this is not the case, and the framework heavily depends on reflection. This means that any logic triggered by the constructor will not occur when deserializing an ordinary Java object. Read my previous blog post for more information. For Records, there is no magic involved when recreating the object when deserializing. If for any reason you put validation logic in the constructor of a Record, we now know this will logic will be applied,

Nevertheless, we can debate if you should put any logic in a record at all. Doing so wrongly can create gadgets that can play a role in a deserialization gadget chain. More importantly, we still use the readObject() function to deserialize. This means that we are still vulnerable to gadget chains in normal POJOs regardless of records.

2. Deserialization Filters in Java

To address deserialization vulnerabilities in Java, it is possible to set serialization filters. This was introduced in Java 9 with the implementation of JEP 290. You can place limits on array sizes, graph depth, total references and stream size. In addition, you can create block and allow lists based on a pattern to limit the classes you want to get deserialized.

You can set such a filter as a global JVM filter or individually per stream. For the global filter, you can set a JVM argument or set it in code. Below I created a filter that allows all classes from mypackage and blocks everything else.

JVM argument:

-Djdk.serialFilter=nl.brianvermeer.example.*;!*


Code:

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example.*;!*");
ObjectInputFilter.Config.setSerialFilter(filter);

You can also set a filter for a specific stream like below.

ObjectInputStream in = new ObjectInputStream(fileIn);
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("nl.brianvermeer.example2.Object;!*");
in.setObjectInputFilter(filesOnlyFilter);

Up to Java 17, when I set a filter on a specific stream, the global filter is overridden for that stream. It doesn’t combine the global filter with the stream-specific filter whatsoever. This is not a very flexible way of working. Moreover, it introduces the issue that your global filter might not work when a library you include does deserialization for you.

Context-Specific Deserialization Filters in Java 17

Java 17 enhanced the deserialization filter with the implementation of JEP 415. 

One of the most important things you can do now is setting aSerialFilterFactory to the ObjectInputFilter.Config. This factory needs to be a BinaryOperator and describes what to do when a specific filter is added to a particular stream.

In the example below, I set a very basic factory to the Config that uses the default merge method to merge the existing filter with the new filter. With this tool I can decide if and filters should be merged or not, and how. This now solves the problem I discussed before, on how your libraries handle the global filter.

ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

Next to the Filter Factory, Java 17 also gives you some nice convenience methods for easy filter creation. Function like allowFilter() and rejectFilter() on ObjectInputFilter are in my opinion a more declarative and readable way of creating filters.

In the Java 17 code example below I am using these new features. In the deserialize method in this example, I specifically reject the Gadget class. Both the Gadget class and the TwoValue record are part of the same package. The Gadget will now be rejected and all other classes in this package will be allowed by the filter.

public static void main(String[] args) throws IOException, ClassNotFoundException {
   var filename = "file.ser";
   var value = new TwoValue("one", "two");
   //var value = new Gadget(new Command("ls -l")); //This will not be deserialized


   var filter1 = ObjectInputFilter.allowFilter(cl -> cl.getPackageName().contentEquals("nl.brianvermeer.example.serialize.records"), ObjectInputFilter.Status.REJECTED);
   ObjectInputFilter.Config.setSerialFilter(filter1);
   ObjectInputFilter.Config.setSerialFilterFactory((f1, f2) -> ObjectInputFilter.merge(f2,f1));

   serialize(value, filename);
   deserialize(filename);
}

public static void serialize(Object value, String filename) throws IOException {
   System.out.println("---serialize");
   FileOutputStream fileOut = new FileOutputStream(filename);
   ObjectOutputStream out = new ObjectOutputStream(fileOut);
   out.writeObject(value);
   out.close();
   fileOut.close();
}

public static void deserialize(String filename) throws IOException, ClassNotFoundException {
   System.out.println("---deserialize");
   FileInputStream fileIn = new FileInputStream(filename);
   ObjectInputStream in = new ObjectInputStream(fileIn);
   ObjectInputFilter intFilter = ObjectInputFilter.rejectFilter(cl -> cl.equals(Gadget.class), ObjectInputFilter.Status.UNDECIDED);
   in.setObjectInputFilter(intFilter);
   TwoValue tv = (TwoValue) in.readObject();
   System.out.println(tv);

3. Java Flight Recorder Deserialization Events

The release of Java 17 also comes with a nice edition to the Java Flight Recorder (JFR) to help you in your crusade against deserialization exploits. This new Java version now supports a specific event to monitor deserialization. A deserialization event will be created for every Object in a stream and records all sorts of interesting things like: the actual type, if there was a filter, if the object was filtered, the object depth, the number of references etc.

All the information is handy to see if there is deserialization somewhere in your application and what is actually getting deserialized while your process is running. You need to make sure, however, to enable this event. It will not get captured by default, so you have to make a specific configuration similar to this.

​​<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" description="test">
   <event name="jdk.Deserialization">
      <setting name="enabled">true</setting>
      <setting name="stackTrace">false</setting>
   </event>
</configuration>

Let use the previous code example, but now deserializing the Gadget class. I get the following result when I execute the code in my IntelliJ IDEA with the Java Flight Recorder and the custom configuration.

You see the event captures the actual object type when deserializing and that the filter rejects this object. If you want to know more in-depth how to use this specific Deserialisation event for the JFR, take a look at the blog post: Monitoring Deserialization to Improve Application Security by Chris Hegarty 


Upgrade to Java 17 for more powerful tools against insecure deserialization exploits

The Java 17 LTS release brings you significant improvements to prevent malicious deserialization in your Java applications. Therefore, upgrading to a newer version like 17 is essential if you want to adopt these practices. Still, with regards to Java’s custom serialization, it is better to avoid it at all in my opinion. However, if you are forced to do this or rely on a library that executed Java’s custom deserialization, you know how to protect yourself.

Also, be aware not to import libraries that contain either known deserialization gadget chains or have other deserialization security issues.

This article was originally posted on the Snyk.io blog: https://snyk.io/blog/new-java-17-features-for-improved-security-and-serialization/ and is used with permission.

Related Articles

View All
  • Are Java Security Updates Important?

    Recently, I was in discussion with a Java user at a bank about the possibilities of using Azul Platform Core to run a range of applications. 

    Security is a very serious concern when sensitive data is in use, and potentially huge sums of money could be stolen.

    I was, therefore, somewhat taken aback when the user said, “We’re not worried about installing Java updates as our core banking services are behind a firewall.”

    Read More
    Aug 03, 2021
  • JEP 411: What it Means for Java’s Security Model and Why You Should Apply the Principle of Least Privilege

    Java, like most platforms or languages has layers of security. This article intends to look at Java’s Authorization layer, which is unlike in other languages.

    We will also distinguish between two different ways this layer is typically utilized and why one is effective while the other isn’t.

    Furthermore, we investigate why JEP 411 only considers the least effective method and hopefully we will increase awareness of the Principle of Least Privilege as it is applied to Java Authorization, improve adoption and encourage people to take advantage of the improved security it provides.

    We hope to prolong its support and possibly even improve it in future.

    Read More
    Avatar photo
    Jun 03, 2021
  • Secure Code Review Best Practices (Part 1)

    Code reviews are hard to do well. Particularly when you’re not entirely sure about the errors you should be looking for!

    Be sure when you’re reviewing code to understand that all code isn’t written equal! Think also about what lies behind the code that you’re reviewing and thus the data and assets you are trying to protect. This working knowledge is something that isn’t easy to add into a checklist.

    However, using the tips below, alongside your domain knowledge, will assist you in deciding where you should spend more of your time and where you should expect higher risk and different types of attacks.

    Read More
    Mar 11, 2021

Author(s)

  • Brian Vermeer

    Java Champions & Developer Advocate and Software Engineer for Snyk. Passionate about Java, (Pure) Functional Programming, and Cybersecurity. Co-leading the Virtual JUG, NLJUG and DevSecCon community. Brian is also an ... Learn more

Comments (0)

Your email address will not be published.

Highlight your code snippets using [code lang="language name"] shortcode. Just insert your code between opening and closing tag: [code lang="java"] code [/code]. Or specify another language.

Save my name, email, and website in this browser for the next time I comment.

Subscribe to foojay updates:

https://foojay.io/feed/
Copied to the clipboard