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.