JEP draft: Dynamic Deserialization Filters

Authorrriggs
OwnerRoger Riggs
TypeFeature
ScopeSE
StatusSubmitted
Releasetbd
Componentcore-libs / java.io:serialization
EffortS
DurationS
Reviewed byBrian Goetz, Chris Hegarty
Created2021/03/10 15:36
Updated2021/04/07 17:21
Issue8263381

Dynamic Deserialization Filters

Dynamic Deserialization Filtering builds on JEP 290 to improve protection of domain specific execution contexts. Filtering policy can be targeted to an application or framework as needed. Dynamic deserialization filtering enables effective allow and reject list protection and flexible configuration without restarting the Java runtime.

Goals

Non-Goals

Success Metrics

Motivation

Experience with JEP 290 has shown that its simple configuration mechanism has limitations in applications with layers of libraries and multiple execution contexts. The most effective preventive measures have a specific allow list for each stream that takes into consideration the context of the application, module, or library. Using the existing system-wide configurable filter for every ObjectInputStream forces the filter to cover all execution contexts in the application. Such a filter may be overly inclusive or overly restrictive. Providing a custom filter for specific functions and execution contexts reduces the risk of deserialization anomalies.

Existing techniques to specify per stream filters have required bytecode instrumentation of each stream constructor to examine the execution contexts at runtime and map the context to an appropriate filter. These techniques are intrusive, brittle, and create a maintenance burden.

A system-wide filter factory enables it to be retrofitted into existing applications using serialization without modification of code for individual deserialization streams.

Description

Dynamic Deserialization Filtering is used to select the filter for each stream when the stream is created, in the constructor or before first invocation to read objects. The filter factory can be provided by the application and is configured to apply across all threads and frameworks. The factory is designed and implemented for the specific application purpose and should take into account all deserialization execution contexts that are to be protected within the Java runtime. The filter factory is invoked in the constructor of ObjectInputStream to select and return the deserialization filter for the stream.

For simple cases, the filter factory can return a fixed filter for an entire application. To support an application with multiple execution contexts the filter factory can better protect individual execution contexts by providing a custom filter for each.
The filter factory can identify the execution contexts from the one or more of thread local state, the hierarchy of callers, modules or libraries, and/or the class loaders that host the classes. At that point, a policy for creating or selecting filters can determine a specific filter based on as much or as little of the available thread context. If a filter factory is not set, a built-in factory, implementing the JEP 290 specification, returns the static system-wide filter if one is configured or has been set.

API Enhancement

In its simplest form, methods are added to set and get the filter factory in the ObjectInputFilter.Config class. ObjectInputStream gets the filter factory and invokes it from the constructor and setObjectInputFilter method.

/**
     * Return the system-wide deserialization filter factory.
     *
     * @return the system-wide serialization filter factory; non-null
     */
    public static BiFunction<ObjectInputFilter, ObjectInputFilter, ObjectInputFilter> getSerialFilterFactory();

   /**
     * Set the system-wide deserialization filter factory.
     *
     * The filter factory is a function of two parameters, the current filter and a requested filter,
     * that returns the filter to be used for the stream.
     *
     * @param filterFactory the serialization filter factory to set as the system-wide filter factory; not null
     */
    public static void setSerialFilterFactory(
                BiFunction<ObjectInputFilter, ObjectInputFilter, ObjectInputFilter> filterFactory);

Dynamic Deserialization Filter Example

Dynamic Deserialization filtering is used in this example to apply a filter to every deserialization in a thread. All of the choices that determine which filter to apply and when are provided by the application allowing the application to fully customize what is filtered.

ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("example.*;!*");
    FilterInThread.doWithSerialFilter(filter, () -> {
          byte[] bytes = ...;
          var o = deserializeObject(bytes);
    });

The FilterInThread class initializes the filter factory, provides the filter factory function that selects the filter for each stream, and the doWithSerialFilter function to apply the filter to every deserialization.

public class FilterInThread {
    
    static {
        // Set the application provided function as the system-wide filter factory
        ObjectInputFilter.Config.setSerialFilterFactory(FilterInThread::apply);
    }

    // The per thread filter to apply
    private static final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();

    /**
     * Apply the filter and invoke the runnable.
     *
     * @param filter the serial filter to apply to every deserialization in the thread
     * @param runnable a Runnable to invoke
     */
    public static void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
        var prevFilter = filterThreadLocal.get();
        try {
            filterThreadLocal.set(filter);
            runnable.run();
        } finally {
            filterThreadLocal.set(prevFilter);
        }
    }
    
    /**
     * This function selects the filter to be used for an ObjectInputStream.
     * When the stream is first created, the thread filter is returned.
     * If a per stream filter is set, a filter is created that combines
     * the results of invoking each filter.
     *
     * @param curr the current filter on the stream
     * @param next a per stream filter
     * @return the selected filter
     */
    public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
        var threadFilter = filterThreadLocal.get();
        var newFilter = (curr == null) ? threadFilter  //  Initial filter for ObjectInputStream
                : FilterCombiner.newCombiner(curr, next);
        return newFilter;
    }
}

When the filter factory is invoked during the construction of each ObjectInputStream, it retrieves the filter from the ThreadLocal and uses it to filter the stream. If a stream specific filter is set with `ObjectInputStream.setObjectFilter, a new filter is created that combines the new filter with the thread filter so that each of the filters is invoked such that if either filter rejects a class, the result is rejected. If either filter allows the class, then it is allowed otherwise it is undecided.

Alternatives

In JEP 290, applications have the ability to set the deserialization filter on each ObjectInputStream. However, configuring each one individually and statically does not scale and is harder to update when new classes need to be added to the reject and allow lists.

JEP 290 supports filters implemented as a Java class allowing complex logic and context awareness. Stream specific filters could be implemented through the use of a delegating filter that is set on every stream. To determine the filter for the specific stream, it would need to examine its caller and map the caller to a specific filter and then delegate to that filter. However, both code complexity and overhead of determining the caller impact performance on every invocation.

Testing

Testing includes the getting and setting of the filter factory and in the various use cases of supplying the filter to the constructor, replacing it when ObjectInputStream.setInputFilter is called, and determining that the filter is called in each case.

Existing tests verify compatibility with JEP 290 specification and implementation.

Risks and Assumptions

There is a small risk that the filter factory, when invoked from the ObjectInputStream constructor, does not have sufficient information to identify a specific filter for the calling context.

Dependencies

The new filter factory is built on the existing serialization specifications and serialization filter specification. JEP 290 - Serialization Filtering