Tackling context-specific deserialization filters – Java I/O: Context-Specific Deserialization Filters
135. 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.
Apply 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.
Apply a Filter Factory to all applications in a process
For applying a Filter Factory to all applications in a process we should follow two steps (again, we don’t touch the application code):
Open in an editor (for instance, Notepad, Wordpad) the java.security file. In JDK 6-8 this file is located in $JAVA_HOME/lib/security/java.security while in JDK 9+, is in $JAVA_HOME/conf/security/java.security.
Edit this file by appending the Filter Factory to the jdk.serialFilterFactory Security Property.
Apply a Filter Factory via ObjectInputFilter.Config
Alternatively, a Filter Factory can be set directly in code via ObjectInputFilter.Config as follows:
ObjectInputFilter.Config
.setSerialFilterFactory(FilterFactoryInstance);
The FilterFactoryInstance argument is an instance of a Filter Factory. This Filter Factory will be applied to all streams from the current application.
Implementing a Filter Factory
A Filter Factory is implemented as a BinaryOperator<ObjectInputFilter>. The apply(ObjectInputFilter current, ObjectInputFilter next) method provides the current filter and the next or requested filter.In order to see how it works let’s assume that we have the following three filters:
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;
}
public static ObjectInputFilter packageFilter() {
return ObjectInputFilter.Config.createFilter(
“modern.challenge.*;!*”);
}
}
The Filters.allowMelonFilter() is set as a stream-global filter as follows:
ObjectInputFilter.Config.setSerialFilter(
Filters.allowMelonFilter());
The Filters.rejectMuskmelonFilter() is set as a stream-specific filter as follows:
Melon melon = new Melon(“Melon”, 2400);
// serialization
byte[] melonSer = Converters.objectToBytes(melon);
// deserialization
Melon melonDeser = (Melon) Converters.bytesToObject(
melonSer, Filters.rejectMuskmelonFilter());
And, the Filters.packageFilter() is set in the Filter Factory as follows:
public class MelonFilterFactory implements
BinaryOperator<ObjectInputFilter> {
@Override
public ObjectInputFilter apply(
ObjectInputFilter current, ObjectInputFilter next) {
System.out.println();
System.out.println(“Current filter: ” + current);
System.out.println(“Requested filter: ” + next);
if (current == null && next != null) {
return ObjectInputFilter.merge(
next, Filters.packageFilter());
}
return ObjectInputFilter.merge(next, current);
}
}
The MelonFilterFactory is set via ObjectInputFilter.Config before any deserialization takes place:
MelonFilterFactory filterFactory = new MelonFilterFactory(); ObjectInputFilter.Config
.setSerialFilterFactory(filterFactory);
Now that everything is in place let’s see what’s happening. The apply() method is called twice. The first time is called when the ObjectInputStream ois is created and we obtain the following output:
Current filter: null
Requested filter:
predicate(modern.challenge.Filters$$Lambda$4/0x000000080
1001800@ba8a1dc, ifTrue: ALLOWED, ifFalse: REJECTED)
The current filter is null, and the requested filter is Filters.allowMelonFilter(). Since the current filter is null and the requested filter is not null, we decided to return a filter as the result of merging the status of the requested filter with the status of the Filters.packageFilter().The second time when the apply() method is called happens when the ois.setObjectInputFilter(filter) is called in Converters.bytesToObject(byte[] bytes, ObjectInputFilter filter). We have the following output:
Current filter: merge(predicate(modern.challenge.Filters$$Lambda$4/0x0000000801001800@ba8a1dc, ifTrue: ALLOWED, ifFalse:REJECTED), modern.challenge.*;!*)
Requested filter: predicate(modern.challenge.Filters$$Lambda$10/0x0000000801002a10@484b61fc, ifTrue: REJECTED, ifFalse:UNDECIDED)
This time, the current and requested filters are not null, so we merge their statuses again. Finally, all filters are successfully passed and the deserialization takes place.