Search icon
Arrow left icon
All Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletters
Free Learning
Arrow right icon
Java Coding Problems - Second Edition

You're reading from  Java Coding Problems - Second Edition

Product type Book
Published in Mar 2024
Publisher Packt
ISBN-13 9781837633944
Pages 798 pages
Edition 2nd Edition
Languages
Author (1):
Anghel Leonard Anghel Leonard
Profile icon Anghel Leonard

Table of Contents (16) Chapters

Preface 1. Text Blocks, Locales, Numbers, and Math 2. Objects, Immutability, Switch Expressions, and Pattern Matching 3. Working with Date and Time 4. Records and Record Patterns 5. Arrays, Collections, and Data Structures 6. Java I/O: Context-Specific Deserialization Filters 7. Foreign (Function) Memory API 8. Sealed and Hidden Classes 9. Functional Style Programming – Extending APIs 10. Concurrency – Virtual Threads and Structured Concurrency 11. Concurrency ‒ Virtual Threads and Structured Concurrency: Diving Deeper 12. Garbage Collectors and Dynamic CDS Archives 13. Socket API and Simple Web Server 14. Other Books You May Enjoy
15. Index

Java I/O: Context-Specific Deserialization Filters

This chapter includes 13 problems related to Java serialization/deserialization processes. We start with classical problems like serializing/deserializing objects to byte[], String, and XML. We continue with JDK 9 deserialization filters meant to prevent deserialization vulnerabilities, and we finish with JDK 17 (JEP 415, final) context-specific deserialization filters.

At the end of this chapter, you’ll be skilled in solving almost any problem related to serializing/deserializing objects in Java.

Problems

Use the following problems to test your programming prowess on Java serialization/deserialization. I strongly encourage you to give each problem a try before you turn to the solutions and download the example programs:

  1. Serializing objects to byte arrays: Write a Java application that exposes two helper methods for serializing/deserializing objects to/from byte[].
  2. Serializing objects to strings: Write a Java application that exposes two helper methods for serializing/deserializing objects to/from String.
  3. Serializing objects to XML: Exemplify at least two approaches for serializing/deserializing objects to/from XML format.
  4. Introducing JDK 9 deserialization filters: Provide a brief introduction to JDK 9 deserialization filters including some insights into the ObjectInputFilter API.
  5. Implementing a custom pattern-based ObjectInputFilter: Provide an example of implementing and setting a custom pattern-based filter via the ObjectInputFilter API...

131. Serializing objects to byte arrays

In Chapter 4, Problem 94, we talked about the serialization/deserialization of Java records, so you should be pretty familiar with these operations. In a nutshell, serialization is the process of transforming an in-memory object into a stream of bytes that can also be stored in memory or written to a file, network, database, external storage, and so on. Deserialization is the reverse process, that is, recreating the object state in memory from the given stream of bytes.

A Java object is serializable if its class implements java.io.Serializable (or, java.io.Externalizable). Accomplishing serialization/deserialization takes place via the java.io.ObjectOutputStream and java.io.ObjectInputStream classes and writeObject()/readObject() methods.

For instance, let’s assume the following Melon class:

public class Melon implements Serializable {
  private final String type;
  private final float weight;
  // constructor, getters
}
...

132. Serializing objects to strings

In the previous problem, you saw how to serialize objects to byte arrays. If we work a little bit on a byte array, we can obtain a string representation of serialization. For instance, we can rely on java.util.Base64 to encode a byte array to String as follows:

public static String objectToString(Serializable obj) throws IOException {       
    
    try ( ByteArrayOutputStream baos = new ByteArrayOutputStream();
          ObjectOutputStream ois = new ObjectOutputStream(baos)) {
        ois.writeObject(obj);
        
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }                
}

A possible output looks like this:

rO0ABXNyABZtb2Rlcm4uY2hhbGxlbmdlLk1lbG9u2WrnGA2MxZ4CAAJGAAZ3ZWlnaHRMAAR0eXBldAAST GphdmEvbGFuZy9TdHJpbmc7eHBFHEAAdAADR2Fj

And, the code to obtain such a String is as follows:

String melonSer = Converters.objectToString(melon);

The reverse process relies on the Base64 decoder as...

133. Serializing objects to XML

Serializing/deserializing objects to XML via the JDK API can be accomplished via java.beans.XMLEncoder, respectively XMLDecoder. The XMLEncoder API relies on Java Reflection to discover the object’s fields and write them in XML format. This class can encode objects that respect the Java Beans contract (https://docs.oracle.com/javase/tutorial/javabeans/writing/index.html). Basically, the object’s class should contain a public no-arguments constructor and public getters and setters for private/protected fields/properties. Implementing Serializable is not mandatory for XMLEncoder/XMLDecoder, so we can serialize/deserialize objects that don’t implement Serializable. Here, it is a helper method that encodes the given Object to XML:

public static String objectToXML(Object obj) 
              throws IOException {
 ByteArrayOutputStream baos = new ByteArrayOutputStream();
 try ( XMLEncoder encoder = new XMLEncoder(
           ...

134. Introducing JDK 9 deserialization filters

As you know from Chapter 4, Problem 94, deserialization is exposed to vulnerabilities that may cause serious security issues. In other words, between serialization–deserialization cycles, an untrusted process (attacker) can modify/alter the serialization form to execute arbitrary code, sneak in malicious data, and so on.

In order to prevent such vulnerabilities, JDK 9 has introduced the possibility of creating restrictions via filters meant to accept/reject deserialization based on specific predicates. A deserialization filter intercepts a stream that expects to be deserialized and applies to it one or more predicates that should be successfully passed in order to proceed with deserialization. If a predicate fails, then deserialization doesn’t even start and the stream is rejected.

There are two kinds of filters:

  • JVM-wide filters: Filters applied to every deserialization that takes place in the JVM. The...

135. Implementing a custom pattern-based ObjectInputFilter

Let’s assume that we already have the Melon class and the helper methods for serializing/deserializing objects to/from byte arrays from Problem 131.

Creating a pattern-based filter via the ObjectInputFilter API can be done by calling the Config.createFilter(String pattern) method. For instance, the following filter rejects the modern.challenge.Melon class:

ObjectInputFilter melonFilter = ObjectInputFilter.Config
  .createFilter("!modern.challenge.Melon;");

We can set this filter as a stream-global filter via setSerialFilter() as follows:

ObjectInputFilter.Config.setSerialFilter(melonFilter);

If we need to get access to a stream-global filter, then we can call getSerialFilter():

ObjectInputFilter serialFilter = 
  ObjectInputFilter.Config.getSerialFilter();

Any stream deserialization in this application will pass through this filter, which will reject any instance...

136. Implementing a custom class ObjectInputFilter

Let’s assume that we already have the Melon class and the helper methods for serializing/deserializing objects to/from byte arrays from Problem 131.

An ObjectInputFilter can be written via a dedicated class by implementing the ObjectInputFilter functional interface as in the following example:

public final class MelonFilter implements ObjectInputFilter {
 @Override
 public Status checkInput(FilterInfo filterInfo) {
 Class<?> clazz = filterInfo.serialClass();
 if (clazz != null) {
  // or, clazz.getName().equals("modern.challenge.Melon")
  return 
   !(clazz.getPackage().getName().equals("modern.challenge")
     && clazz.getSimpleName().equals("Melon"))
     ? Status.ALLOWED : Status.REJECTED;
 }
 return Status.UNDECIDED;
 }
}

This filter is exactly the same as the pattern-based filter, !modern.challenge.Melon, only that it is expressed via Java Reflection.

We can...

137. Implementing a custom method ObjectInputFilter

Let’s assume that we already have the Melon class and the helper methods for serializing/deserializing objects to/from byte arrays from Problem 131.

An ObjectInputFilter can be written via a dedicated method as in the following example:

public final class Filters {
 private Filters() {
  throw new AssertionError("Cannot be instantiated");
 }
 public static ObjectInputFilter.Status melonFilter(
               FilterInfo info) {
  Class<?> clazz = info.serialClass();
  if (clazz != null) {
   // or, clazz.getName().equals("modern.challenge.Melon")
   return 
    !(clazz.getPackage().getName().equals("modern.challenge")
      && clazz.getSimpleName().equals("Melon"))
      ? Status.ALLOWED :Status.REJECTED;
  }
  return Status.UNDECIDED;
 }
}

Of course, you can add more filters in this class.

We can set this filter as a stream-global filter as follows:

...

138. Implementing a custom lambda ObjectInputFilter

Let’s assume that we already have the Melon class and the helper methods for serializing/deserializing objects to/from byte arrays from Problem 131.

An ObjectInputFilter can be written via a dedicated lambda and set as a stream-global filter as follows:

ObjectInputFilter.Config
  .setSerialFilter(f -> ((f.serialClass() != null)
  // or, filter.serialClass().getName().equals(
  //     "modern.challenge.Melon")
  && f.serialClass().getPackage()
                   .getName().equals("modern.challenge")
  && f.serialClass().getSimpleName().equals("Melon"))
  ? Status.REJECTED : Status.UNDECIDED);

Or, as a stream-specific filter as follows:

Melon melonDeser = (Melon) Converters.bytesToObject(melonSer, 
  f -> ((f.serialClass() != null)
   // or, filter.serialClass().getName().equals(
   //       "modern.challenge.Melon")
  && f.serialClass()...

139. Avoiding StackOverflowError at deserialization

Let’s consider the following snippet of code:

// 'mapOfSets' is the object to serialize/deserialize
HashMap<Set, Integer> mapOfSets = new HashMap<>();
Set<Set> set = new HashSet<>();
mapOfSets.put(set, 1);
set.add(set);

We plan to serialize the mapOfSets object as follows (I assume that Converters.objectToBytes() is well-known from the previous problems):

byte[] mapSer = Converters.objectToBytes(mapOfSets);  

Everything works just fine until we try to deserialize mapSer. At that moment, instead of a valid object, we will get a StackOverflowError as follows:

Exception in thread "main" java.lang.StackOverflowError
  at java.base/java.util.HashMap$KeyIterator
    .<init>(HashMap.java:1626)
  at java.base/java.util.HashMap$KeySet
    .iterator(HashMap.java:991)
  at java.base/java.util.HashSet
    .iterator(HashSet.java:182)
  at java.base/java.util.AbstractSet...

140. Avoiding DoS attacks at deserialization

Denial-of-service (DoS) attacks are typically malicious actions meant to trigger, in a short period of time, a lot of requests to a server, application, and so on. Generally speaking, a DoS attack is any kind of action that intentionally/accidentally overwhelms a process and forces it to slow down or even crash. Let’s see a snippet of code that is a good candidate for representing a DoS attack in the deserialization phase:

ArrayList<Object> startList = new ArrayList<>();
List<Object> list1 = startList;
List<Object> list2 = new ArrayList<>();
for (int i = 0; i < 101; i++) {
  List<Object> sublist1 = new ArrayList<>();
  List<Object> sublist2 = new ArrayList<>();
  sublist1.add("value: " + i);
  list1.add(sublist1);
  list1.add(sublist2);
  list2.add(sublist1);
  list2.add(sublist2);
  list1 = sublist1;
  list2 = sublist2;
}

We plan to serialize the startList...

141. Introducing JDK 17 easy filter creation

Starting with JDK 17, we can express filters more intuitively and readably via two convenient methods named allowFilter() and rejectFilter(). And, since the best way to learn is with an example, here is a usage case of these two convenient methods:

public final class Filters {
 private Filters() {
  throw new AssertionError("Cannot be instantiated");
 }
 public static ObjectInputFilter allowMelonFilter() {
  ObjectInputFilter filter = ObjectInputFilter.allowFilter( 
   clazz -> Melon.class.isAssignableFrom(clazz),
           ObjectInputFilter.Status.REJECTED);
   return filter;
 }
 public static ObjectInputFilter rejectMuskmelonFilter() {
  ObjectInputFilter filter = ObjectInputFilter.rejectFilter( 
   clazz -> Muskmelon.class.isAssignableFrom(clazz),
           ObjectInputFilter.Status.UNDECIDED);
   return filter;
  }
}

The allowMelonFilter() relies on ObjectInputFilter.allowFilter() to allow only objects that...

142. Tackling context-specific deserialization filters

JDK 17 enriched the deserialization filter capabilities with the implementation of JEP 415, Context-Specific Deserialization Filters.

Practically, JDK 17 added the so-called Filter Factories. Depending on the context, a Filter Factory can dynamically decide what filters to use for a stream.

Applying a Filter Factory per application

If we want to apply a Filter Factory to a single run of an application, then we can rely on the jdk.serialFilterFactory system property. Without touching the code, we use this system property at the command line as in the following example:

java -Djdk.serialFilterFactory=FilterFactoryName YourApp

The FilterFactoryName is the fully qualified name of the Filter Factory, which is a public class that can be accessed by the application class loader, and it was set before the first deserialization.

Applying a Filter Factory to all applications in a process

To apply a Filter...

143. Monitoring deserialization via JFR

Java Flight Recorder (JFR) is an event-based tool for the diagnosis and profiling of Java applications. This tool was initially added in JDK 7 and, since then, it has been constantly improved. For instance, in JDK 14, JFR was enriched with event streaming, and in JDK 19, with filtering event capabilities, and so on. You can find all JFR events listed and documented per JDK version at https://sap.github.io/SapMachine/jfrevents/.

Among its rich list of events, JFR can monitor and record deserialization events (the deserialization event). Let’s assume a simple application like the one from Problem 131 (the first problem of this chapter). We start configuring JFR for monitoring the deserialization of this application by adding deserializationEvent.jfc into the root folder of the application:

<?xml version="1.0" encoding="UTF-8"?>
<configuration version="2.0" description="test">
...

Summary

In this chapter, we covered a bunch of problems dedicated to handling Java serialization/deserialization processes. We started with classical problems and moved on to cover JDK 17 context-specific deserialization filters passing through JDK 9 deserialization filters on the way.

Join our community on Discord

Join our community’s Discord space for discussions with the author and other readers:

https://discord.gg/8mgytp5DGQ

lock icon The rest of the chapter is locked
You have been reading a chapter from
Java Coding Problems - Second Edition
Published in: Mar 2024 Publisher: Packt ISBN-13: 9781837633944
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $15.99/month. Cancel anytime}