JDK-8263381 : JEP 415: Context-Specific Deserialization Filters
  • Type: JEP
  • Component: core-libs
  • Sub-Component: java.io:serialization
  • Priority: P4
  • Status: Closed
  • Resolution: Delivered
  • Fix Versions: 17
  • Submitted: 2021-03-10
  • Updated: 2021-08-02
  • Resolved: 2021-08-02
Related Reports
Blocks :  
Relates :  
Description
Summary
-------

Allow applications to configure context-specific and dynamically-selected
deserialization filters via a JVM-wide filter factory that is invoked to select
a filter for each individual deserialization operation.


Non-Goals
---------

- It is not a goal to define policies for deserialization filter selection.

- It is not a goal to define a mechanism for the configuration or distribution
  of filters.


Motivation
----------

Deserializing untrusted data is an inherently dangerous activity because the
content of the incoming data stream determines the objects that are created,
the values of their fields, and the references between them.  In many typical
uses the bytes in the stream are received from an unknown, untrusted, or
unauthenticated client.  By careful construction of the stream, an adversary
can cause code in arbitrary classes to be executed with malicious intent.  If
object construction has side effects that change state or invoke other actions,
those actions can compromise the integrity of application objects, library
objects, and even the Java runtime.  The key to disabling deserialization
attacks is to prevent instances of arbitrary classes from being deserialized,
thereby preventing the direct or indirect execution of their methods.

We introduced [deserialization filters (JEP 290)][jep290] in Java 9 to
enable application and library code to [validate incoming data
streams][validate] before deserializing them.  Such code supplies validation
logic as a [`java.io.ObjectInputFilter`][obinfilt] when it creates a
deserialization stream (i.e., a [`java.io.ObjectInputStream`][ois]).

Relying on a stream's creator to explicitly request validation has several
limitations.  This approach does not scale, and makes it difficult to update
filters after code has been shipped.  It also cannot impose filtering on
deserialization operations performed by third-party libraries in an
application.

To address these limitations, JEP 290 also introduced a JVM-wide
deserialization filter which can be set via an API, system properties, or
security properties.  This filter is _static_ since it is specified exactly
once, at startup.  Experience with the static JVM-wide filter has revealed that
it, too, has limitations, particularly in complex applications with layers of
libraries and multiple execution contexts.  Using the JVM-wide filter for every
`ObjectInputStream` requires the filter to cover every execution context in the
application, so the filter usually winds up being either too inclusive or too
restrictive.

A better approach would be to configure per-stream filters in a way that does
not require the participation of every stream creator.

To protect the JVM against deserialization vulnerabilities, application
developers need a clear description of the objects that can be serialized or
deserialized by each component or library. For each context and use case,
developers should construct and apply an appropriate filter.  For example, if
the application uses a specific library to deserialize a particular cohort of
objects then a filter for the relevant classes can be applied when calling the
library.  Creating an allow-list of classes, and rejecting everything else,
gives protection against objects in a stream that are otherwise unknown or
unexpected.  Encapsulation or other natural application or library partitioning
boundaries can be used to narrow the set of objects that are allowed or
definitely not allowed.  If it is not practical to have an allow-list then a
reject-list should include classes, packages, and modules that are known not to
occur in the stream or are known to be malicious.

An application���s developer is in the best position to understand the structure
and operation of the application���s components.  This enhancement enables the
application developer to construct and apply filters to every deserialization
operation.

[jep290]: https://openjdk.java.net/jeps/290
[obinfilt]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/ObjectInputFilter.html
[validate]: https://docs.oracle.com/en/java/javase/16/core/serialization-filtering1.html#GUID-55BABE96-3048-4A9F-A7E6-781790FF3480
[obinfilt.cf]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/ObjectInputFilter.Config.html
[ois]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/ObjectInputStream.html
[ois.soif]: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/ObjectInputStream.html#setObjectInputFilter(java.io.ObjectInputFilter)


Description
-----------

As noted above, JEP 290 introduced both per-stream deserialization filters and
a static JVM-wide filter.  Whenever an `ObjectInputStream` is created, its
per-stream filter is initialized to be the static JVM-wide filter.  That
per-stream filter can later be changed to a different filter, if desired.

Here we introduce a configurable JVM-wide _filter factory_. Whenever an
`ObjectInputStream` is created, its per-stream filter is initialized to the
value returned by invoking the static JVM-wide filter factory.  Thus these
filters are _dynamic_ and _context-specific_, unlike the single static JVM-wide deserialization
filter.  For backward compatibility, if a filter factory is not set then a
built-in factory returns the static JVM-wide filter if one was configured.

The filter factory is used for every deserialization operation in the Java
runtime, whether in application code, library code, or code in the JDK itself.
The factory is specific to the application and should take into account every
deserialization execution context within the application.
The filter factory is called from the `ObjectInputStream` constructor and also from 
`ObjectInputStream.setObjectInputFilter`. The arguments are the current filter and a new filter.
When called from the constructor, the current filter is `null` and the new filter
is the static JVM-wide filter. The factory determines and returns the initial filter for the
stream.  The factory can create a composite filter with other context-specific controls
or just return the static JVM-wide filter.
If `ObjectInputStream.setObjectInputFilter` is called, the factory
is called a second time with the filter returned from the first call and
the requested new filter.  The factory determines how to combine the two filters
and returns the filter, replacing the filter on the stream.

For simple cases, the filter factory can return a fixed filter for the entire
application.  For example, here is a filter that allows example classes, allows
classes in the `java.base` module, and rejects all other classes:

    var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*")

In an application with multiple execution contexts, the filter factory can
better protect individual contexts by providing a custom filter for each.  When
the stream is constructed, the filter factory can identify the execution
context based upon the current thread-local state, hierarchy of callers,
library, module, and class loader.  At that point, a policy for creating or
selecting filters can choose a specific filter or composition of filters based
on the context.

If multiple filters are present then their results can be combined.  A useful
way to combine filters is to reject deserialization if any of the filters
reject it, allow it if any filter allows it, and otherwise remain undecided.

### Command Line Use

The properties `jdk.serialFilter` and `jdk.serialFilterFactory` can be set on  
the command line to set the filter and filter factory.
The existing `jdk.serialFilter` property sets a pattern based filter.

The `jdk.serialFilterFactory` property is the class name of the filter factory to
be set before the first deserialization. The class must be public and accessible 
to the application class loader. 

For compatibility with JEP 290, if `jdk.serialFilterFactory` property is not set, 
the filter factory is set to a builtin that provides compatibility with earlier 
versions.


### API

We define two methods in the `ObjectInputFilter.Config` class to set and get
the JVM-wide filter factory.  The filter factory is a function with two
arguments, a current filter and a next filter, and it returns a filter.

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

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

### Example

This class shows how to filter to every deserialization operation that takes
place in the current thread.  It defines a thread-local variable to hold the
per-thread filter, defines a filter factory to return that filter, configures
the factory as the JVM-wide filter factory, and provides a utility function
to run a `Runnable` in the context of a specific per-thread filter.

    public class FilterInThread implements BinaryOperator<ObjectInputFilter> {

        // ThreadLocal to hold the serial filter to be applied
        private final ThreadLocal<ObjectInputFilter> filterThreadLocal = new ThreadLocal<>();

        // Construct a FilterInThread deserialization filter factory.
        public FilterInThread() {}

        /**
         * The filter factory, which is invoked every time a new ObjectInputStream
         * is created.  If a per-stream filter is already set then it returns a
         * filter 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) {
            if (curr == null) {
                // Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
                var filter = filterThreadLocal.get();
                if (filter != null) {
                    // Prepend a filter to assert that all classes have been Allowed or Rejected
                    filter = ObjectInputFilter.Config.rejectUndecidedClass(filter);
                }
                if (next != null) {
                    // Prepend the next filter to the thread filter, if any
                    // Initially this is the static JVM-wide filter passed from the OIS constructor
                    // Append the filter to reject all UNDECIDED results
                    filter = ObjectInputFilter.Config.merge(next, filter);
                    filter = ObjectInputFilter.Config.rejectUndecidedClass(filter);
                }
                return filter;
            } else {
                // Called from OIS.setObjectInputFilter with a current filter and a stream-specific filter.
                // The curr filter already incorporates the thread filter and static JVM-wide filter
                // and rejection of undecided classes
                // If there is a stream-specific filter prepend it and a filter to recheck for undecided
                if (next != null) {
                    next = ObjectInputFilter.Config.merge(next, curr);
                    next = ObjectInputFilter.Config.rejectUndecidedClass(next);
                    return next;
                }
                return curr;
            }
        }

        /**
         * 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 void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
            var prevFilter = filterThreadLocal.get();
            try {
                filterThreadLocal.set(filter);
                runnable.run();
            } finally {
                filterThreadLocal.set(prevFilter);
            }
        }
    }

If a stream-specific filter was already set with
`ObjectInputStream::setObjectFilter` then the filter factory combines that
filter with the next filter.  If either filter rejects a class then that class
is rejected. If either filter allows the class then that class is
allowed. Otherwise, the result is undecided.

Here���s a simple example of using the `FilterInThread` class:

        // Create a FilterInThread filter factory and set
        var filterInThread = new FilterInThread();
        ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);

        // Create a filter to allow example.* classes and reject all others
        var filter = ObjectInputFilter.Config.createFilter("example.*;java.base/*;!*");
        filterInThread.doWithSerialFilter(filter, () -> {
              byte[] bytes = ...;
              var o = deserializeObject(bytes);
        });


Alternatives
------------

JEP 290 allows filters to be implemented as Java classes, thereby allowing
complex logic and context awareness.  Context-dependent 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 the overhead of determining the
caller would impact performance on every invocation.

Comments
Perhaps my description inadequately describes the meta-level mechanism that enables a variety of policy choices the application can provide. The example is only one in which a per thread filter is combined with the per stream filter in a particular way. What I've called the filter factory has the function you've called a transformer. It takes the current and proposed filters and returns a filter to be used for the stream. The function can use other context information and can combine filters to fit any needed and produce a filter for the stream.
29-03-2021

So you don't want to set a filter dynamically, you want to restrict the filter already set dynamically. So instead of ``` doWithSerialFilter(filter, () -> { byte[] bytes = ...; var o = deserializeObject(bytes); }); ``` that set a filter, you want to "transform" an existing filter in order to restrict it. So doWithSerialFilter should take a function that transform the filter to add restrictions instead of taking a filter. so it should be something like ``` SerialFilter myRestrictiveFilter = ... doRestrictSerialFilter(filter -> myRestrictiveFilter.andThen(filter), () -> { }); ``` and in the the constructor of ObjectInputStream or in the method ObjectInputStream.setSerialFilter(), you should take the "default" filter (the gobal one or the local one) and then call the function that transforms the filter that you get from the ThreadLocal. Using a function instead of a filter + a predefined way to compose it is a more reusable approach and also a less magic one, as a user, you write how you compose the filters. (in term of implementation, the things to solve is what to do when the global filter is null and you are inside a doRestrictSerialFilter, calling with anempty filter seems a reasonable choice, and how to detect inside ObjectInputStream.setSerialFilter() that you can not set the serialFilter twice (because now the default filter can be a composition of filter not only null and Config.getSerialFilter()))
26-03-2021

The ability for the filter to be context sensitive, may in some cases be more naturally described as dynamically scoped, that is true for the thread local example in the JEP. Factory may not be the best term to describe the function. Perhaps meta-filter. The key element is to add a hook whose function is to return a filter that will be applied to the stream. That filter can be composed from other filters and delegate to the statically define filter factory and the per stream filter. For example, it might invoke the per stream filter and then invoke the static filter if the stream filter does not reject the class. In JEP 290, it would be up the per stream filter to explicitly delegate to the configured global filter to achieve the same effect. The simple composition is most useful with filters that reject undesirable classes. In the description, the factory is provided by the application as a BiFunction, set using a method in the Config class. The factory, if set, replaces the fixed function combination of system-wide filter and stream filter defined by JEP 290 allowing more flexible composition and delegation. It is invoked in the two places that set the filter for a stream in the stream constructor and when setting a per-stream filter. The dynamically scoped filter must be designed to delegate to the global filter and to the per stream filter (and any other filters it chooses based on the runtime context). It implements an application driven policy that takes into account possibly more than two filters. For simple cases, where any filter rejects a class, it just rejects it. If those filters return the status undecided or allows, the dynamic filter determines what to do next. Is that result sufficient or should another filter be invoked. A fixed model of multiple filters is very restrictive especially if it is defined as hiding other filters. Some use cases call for narrowing the set of deserializable classes, not replacing the set. In some cases, for example a serialization function of a library, the caller needs to place restrictions on what is serializable and not leave it to the library. A default method andThen might be useful but would need to clearly define how the results of invoking each filter are combined, especially as relates to the undecided status and whether there is any short circuiting when a filter status is reject.
26-03-2021

The title is more "Dynamically Scoped Deserialization Filters". I don't understand this sentence, there are two "enable", "Enabling a system-wide filter factory enables it to be retrofitted into existing applications using serialization without modification of code for individual deserialization streams." The "Description" talk about a factory, it's not clear which factory it is ? Is it "ObjectInputFilter.Config" ? "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" So the dynamic filter take over a global filter set by setSerialFilter���(), right. So there are 3 kind of filter, a global one, a dynamically scoped one and a one specific (local) to an ObjectInputStream. Why the dynamically scope filter hide the global filter but is combined with the local filter ? It's not logical and it's a behavioral change, setting a local filter has now a different meaning. One way to see the issue is what ObjectInputStream.getInputFilter() should now return when a dynamically scoped filter and a local filter are both set. (BTW the javadoc of getInputFilter() and setInputFilter on ObjectInputStream disagree on the number of time one can call setInputFilter) For me, each filter, global > dynamically scoped > local should hide the previous one, which means that instead of always getting the global scope in the constructor of ObjectInputStream, the dynamically scoped one should be used if it exists. Moreover, a method (a default method) andThen() should be added to ObjectInputFilter to allow users to easily combine ObjectInputFilter if they want. We also need a way to get the dynamically scoped filter if it exist the same way we can query the global filter actually.
26-03-2021