Abstract: JEP 290 introduced the ObjectInputFilter, which we can use to filter objects returned from an ObjectInputStream. We examine three ways of making filters, through subclassing, with factory methods and with text patterns, and then show some interesting edge cases.
Welcome to the 304th edition of The Java(tm) Specialists' Newsletter, once again sent from the wonderful Island of Crete. This is the week that tsikoudia (also called raki in Heraklion, a bit like the Italian grappa) is distilled in our village. They get a license to distill for one week, and they try to get as much distilled as possible. In between carrying heavy buckets of fermented grape skins, there is a lot of waiting involved, and that's a perfect time to chat and enjoy a shot of fresh tsikoudia. Attending a distillation in process should be on everyone's bucket list, even for teetotallers :-) There is something primal about sitting around a blazing fire sporting a hot copper couldron.
javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.
Even at the end of 2022, we still have a fairly large number of companies running their Java systems on Java 8. Their systems work, they earn money from them every day, and usually there is some effort involved in migrating to more modern Java. But it is becoming increasingly difficult to find Java programmers willing to work with such outdated technology. Plus, we need to use pieces of wire and string to work around some issues that were solved years ago. One of our customers asked us to create a course for those wishing to master Java 11. I know, we already have Java 19, so why bother with Java 11? The step from Java 8 to Java 11 can be a big one, because an effective Java programmer should have a perfunctory understanding of how the Java Platform Module System (JPMS or Java Modules) work. That takes roughly a day to explain, together with copious exercises. We then spend another two days taking in the many other changes that Java 9, 10, and 11 gave us. Some of the features only warrant a brief overview and a pointer of where to find out more information. With others, we spend more time exploring and doing exercises. The course is not on my website yet, but if you are interested, please send me an email and I can send you the outline directly. Of course we are also working on upgrades to Java 17 and beyond, which we are creating JIT.
In this newsletter we will explore one of the lesser known
features of Java 9, namely JEP 290: Filter
Incoming Serialization Data. It is summarized as:
"Allow incoming streams of object-serialization data to be
filtered in order to improve both security and
robustness." Java Object Serialization is rare nowadays,
and thus we only give it a cursory
glance in our "Mastering Java 11 Course". However, there are
some fun features and I'd like to expand on them in this
newsletter. Prior to Java 9, there was no way of stopping
input from an ObjectInputStream
. Thus if a rogue process sent
us very large arrays or instances of classes that could be
considered dangerous, we could perhaps check the generated
instances using the ObjectInputValidation
interface, but at
that point the instance had already been constructed. We
might already have run out of memory on the server or been
hacked in some way.
Enter the Java 9 ObjectInputFilter
. It has a method
checkInput(FilterInfo)
, and the FilterInfo
provides the
following information:
serialClass()
: the class that is supposed to be
deserializedarrayLength()
: array length if the object is or contains
an arraydepth()
: the depth of the object graph at that pointreferences()
: the current number of object
referencesstreamBytes()
: the current number of bytes consumed
The method checkInput()
returns either Status.UNDECIDED
,
Status.ALLOWED
, or Status.REJECTED
. As far as I could tell,
we should use ALLOWED if we want the object to be accepted,
no matter what anyone else wants. Similarly we would mark
them as REJECTED if, no matter what other filters say, we
want this to be rejected. UNDECIDED means we allow others to
override our decision. This would probably be our default
status.
For example, we all know how LinkedList
should probably not
be used in projects. Even the author Joshua Bloch famously
tweeted "Does
anyone actually use LinkedList? I wrote it, and I never use
it." We could write an ObjectInputFilter
that
specifically disallows a LinkedList
to be deserialized:
import java.io.ObjectInputFilter; import java.util.LinkedList; public class NoLinkedListFilter implements ObjectInputFilter { @Override public Status checkInput(FilterInfo filterInfo) { return filterInfo.serialClass() == LinkedList.class ? Status.REJECTED : Status.UNDECIDED; } }
When we read in an ObjectInputStream
, we simply set the
ObjectInputFilter
and this prevents LinkedList
from being
deserialized. We can set this on a per-stream basis or
globally with
ObjectInputFilter.Config.setSerialFilter(filter)
. Let's set
up some helper classes and demos to make this easier to
visualize. First we need a company with employees:
import java.io.Serial; import java.io.Serializable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; public class Company implements Serializable { @Serial private static final long serialVersionUID = 0L; private final List<String> employees = new LinkedList<>(); public void addEmployee(String name) { employees.add(name); } /** * Convenience method for you-know-who. */ public void remove75PercentOfEmployees() { int pos = 0; for (var it = employees.iterator(); it.hasNext(); ) { it.next(); if (pos++ % 4 != 0) it.remove(); } } @Override public String toString() { return "Company{employees=" + employees + '}'; } }
We also write a SerializationHelper
to make serializing and
deserializing easier:
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InvalidClassException; import java.io.ObjectInputFilter; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.List; class SerializationHelper { static byte[] serialize(Object... objects) throws IOException { var bout = new ByteArrayOutputStream(); try (var out = new ObjectOutputStream(bout)) { for (Object object : objects) { out.writeObject(object); } out.writeObject(null); } return bout.toByteArray(); } static List<?> deserialize(byte[] bytes) throws IOException, ClassNotFoundException { return deserialize(null, bytes); } static List<?> deserialize(ObjectInputFilter filter, byte[] bytes) throws IOException, ClassNotFoundException { var new ArrayList<>(); try (var in = new ObjectInputStream( new ByteArrayInputStream(bytes) )) { if (filter != null) in.setObjectInputFilter(filter); Object o; while ((o = in.readObject()) != null) { result.add(o); } } catch(InvalidClassException ex) { result.add(ex.toString()); } return List.copyOf(result); } }
Before we show a demo of how we can prevent a LinkedList
from
appearing from an ObjectInputStream
, there are several ways
in which we can create these filters. The first we saw
already, via a subclass. Since it is also a
@FunctionalInterface
, we could also create a lambda. However,
since Java 17, we have two new static factory methods in
ObjectInputFilter
called allowFilter()
and rejectFilter()
.
They both take a Predicate
for a Class
and a status to return
if we do not have a match. We have two other static methods
merge()
and rejectUndecidedClass()
. We can use these to
create a chain of filters, much like the
Chain of
Responsibility Pattern in the Gang-of-Four.
A third way of creating filters is via a text String and by
calling ObjectInputFilter.Config.createFilter(String)
. In this case we can list the classes that we want to accept or reject, together
with some more attributes we will look at later.
We now look at a demo, where we create several filters for
reading our ObjectInputStream
.
import java.io.IOException; import java.io.ObjectInputFilter; import java.io.UncheckedIOException; import java.util.ArrayList; import java.util.LinkedList; public class ObjectInputFilterDemo { public static void main(String... args) throws Exception { Company company = new Company(); company.addEmployee("Chris"); company.addEmployee("Tony"); company.addEmployee("John"); company.addEmployee("Parag"); byte[] bytes = SerializationHelper.serialize(company); // With no filter test(bytes, null); // allowed // With our custom filter var filter1 = new NoLinkedListFilter(); test(bytes, filter1); // rejected // With a generated filter rejecting LinkedList var filter2 = ObjectInputFilter.rejectFilter( clazz -> clazz == LinkedList.class, ObjectInputFilter.Status.UNDECIDED); test(bytes, filter2); // rejected // With a generated filter allowing ArrayList var filter3 = ObjectInputFilter.allowFilter( clazz -> clazz == ArrayList.class, ObjectInputFilter.Status.UNDECIDED); test(bytes, filter3); // allowed // Merging filter1 and filter3 var filter4 = ObjectInputFilter.merge(filter1, filter3); test(bytes, filter4); // rejected // This would reject even the Company class var filter5 = ObjectInputFilter.rejectUndecidedClass(filter3); test(bytes, filter5); // rejected // Create a filter from a text pattern that rejects LinkedList var filter6 = ObjectInputFilter.Config.createFilter( "!java.util.LinkedList"); test(bytes, filter6); // rejected // Create a filter from a text pattern that accepts Company, // LinkedList and rejects all other classes var filter7 = ObjectInputFilter.Config.createFilter( "Company;java.util.LinkedList;!*"); test(bytes, filter7); // allowed ObjectInputFilter.Config.setSerialFilter(filter1); test(bytes, null); // rejected } private static void test(byte[] bytes, ObjectInputFilter filter) throws IOException, ClassNotFoundException { var result = SerializationHelper.deserialize(filter, bytes); System.out.println(result); // rejected } }
The output is as follows:
[Company{employees=[Chris, Tony, John, Parag]}] [java.io.InvalidClassException: filter status: REJECTED] [java.io.InvalidClassException: filter status: REJECTED] [Company{employees=[Chris, Tony, John, Parag]}] [java.io.InvalidClassException: filter status: REJECTED] [java.io.InvalidClassException: filter status: REJECTED] [java.io.InvalidClassException: filter status: REJECTED] [Company{employees=[Chris, Tony, John, Parag]}] [java.io.InvalidClassException: filter status: REJECTED]
The easiest, and probably most versatile, of the ways of creating a filter is with a pattern. In addition to being able to list allowed classes, separated by semicolons, we can also add modules, match classes and subpackages with .** (e.g. java.util.** would allow anything in that package and also subpackages such as java.util.concurrent), .* for classes within a package, and * for all classes. Thus in my filter7, I am allowing LinkedList and Company, but no other classes, by ending the filter with !*. In addition to specifying what classes are allowed, we can also use the properties maxdepth, maxrefs, maxbytes, and maxarray, which correspond to the properties in the FilterInfo parameter.
Curiously, I could not find an easy way to protect against very long Strings (or any Strings for that matter) being sent to me. Here is a demo:
import java.io.IOException; import java.io.ObjectInputFilter; import java.io.ObjectStreamException; import java.util.List; public class NoStringsPlease { public static void main(String... args) throws IOException, ClassNotFoundException { byte[] bytes = SerializationHelper.serialize("Hello world"); ObjectInputFilter filter1 = ObjectInputFilter.rejectFilter( c -> c == String.class, ObjectInputFilter.Status.ALLOWED ); var result = SerializationHelper.deserialize(filter1, bytes); System.out.println(result); ObjectInputFilter filter2 = ObjectInputFilter.Config.createFilter("maxarray=10"); result = SerializationHelper.deserialize(filter2, bytes); System.out.println(result); } }
The code above runs without an issue and outputs:
[Hello world] [Hello world]
The issue, as far as I can tell, is that Strings are not treated as normal Objects in Serialization. Instead, they are written as UTF. When they are deserialized, the filter is applied neither for the String class, nor for the byte[] or char[] inside the String. For example:
import java.io.IOException; import java.io.ObjectInputFilter; import java.io.ObjectStreamException; import java.util.List; public class NoStringsPlease { public static void main(String... args) throws IOException, ClassNotFoundException { byte[] bytes = SerializationHelper.serialize("Hello world"); ObjectInputFilter filter1 = ObjectInputFilter.rejectFilter( c -> c == String.class, ObjectInputFilter.Status.ALLOWED ); var result = SerializationHelper.deserialize(filter1, bytes); System.out.println(result); ObjectInputFilter filter2 = ObjectInputFilter.Config.createFilter("maxarray=10"); result = SerializationHelper.deserialize(filter2, bytes); System.out.println(result); } }
This outputs the following:
[Hello world] [Hello world]
String and primitives are deliberately excluded from the filter,
as described in JEP 290: "The filter is not called for primitives or java.lang.String
instances that are encoded concretely in the stream."
We mentioned earlier that we can also create a filter
from a text pattern. What would you expect
"!java.lang.String"
to filter, if anything?
Turns out that if we deserialize an array, then the pattern
is applied to the actual component type of the array. For
multiple dimensional arrays, the lowest non-array type is
used. Thus if we pass in a String[], then our pattern would
reject that. For example:
import java.io.IOException; import java.io.ObjectInputFilter; import java.util.stream.Stream; public class StrangeFilterDemo { public static void main(String... args) throws IOException, ClassNotFoundException { byte[] bytes = SerializationHelper.serialize((Object) Stream.of("Hello", "world").toArray(String[]::new)); ObjectInputFilter filter1 = ObjectInputFilter.Config.createFilter("!java.lang.String"); var result = SerializationHelper.deserialize(filter1, bytes); System.out.println(result); } }
The output this time is
[java.io.InvalidClassException: filter status: REJECTED]
There is a lot more to write about this ObjectInputFilter, and it seems to be in use within the OpenJDK. Futhermore, new factory methods were added in Java 17. However, this is still a rather niche feature and not sure if I will use it in the real world.
Kind regards from Chorafakia
Heinz
We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)
We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.