You can use the Java serialization filtering mechanism to help prevent deserialization vulnerabilities. You can define pattern-based filters or you can create custom filters.
Topics:
An application that accepts untrusted data and deserializes it is vulnerable to attacks. You can create filters to screen incoming streams of serialized objects before they are deserialized.
Deserializing untrusted data, especially from an unknown, untrusted, or unauthenticated client, 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. By careful construction of the stream, an adversary can run code in arbitrary classes with malicious intent.
For example, if object construction has side effects that change state or invoke other actions, then those actions can compromise the integrity of application objects, library objects, and even the Java runtime. "Gadget classes," which can perform arbitrary reflective actions such as create classes and invoke methods on them, can be deserialized maliciously to cause a denial of service or remote code execution.
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. You can do this through serialization filters.
An object is serialized when its state is converted to a byte
stream. That stream can be sent to a file, to a database, or over
a network. A Java object is serializable if its class or any of
its superclasses implements either the
java.io.Serializable
interface or the
java.io.Externalizable
subinterface. In the JDK,
serialization is used in many areas, including Remote Method
Invocation (RMI), custom RMI for interprocess communication (IPC)
protocols (such as the Spring HTTP invoker), and Java Management
Extensions (JMX).
An object is deserialized when its serialized form is
converted to a copy of the object. It is important to ensure the
security of this conversion. Deserialization is code execution
because the readObject
method of the class that is
being deserialized can contain custom code.
A serialization filter enables you to specify which classes are acceptable to an application and which should be rejected. Filters also enable you to control the object graph size and complexity during deserialization so that the object graph doesn't exceed reasonable limits. You can configure filters as properties or implement them programmatically.
Note: A serialization filter is not enabled
or configured by default. Serialization filtering doesn't occur
unless you have specified the filter in a system property or a
Security Property or set it with the
sun.misc.ObjectInputFilter
class.
Besides creating filters, you can take the following actions to help prevent deserialization vulnerabilities:
readObject
method.Note: Built-in filters are provided for RMI. However, you should use these built-in filters as starting points only. Configure reject-lists and/or extend the allow-list to add additional protection for your application that uses RMI. See Built-in Filters.
For more information about these and other strategies, see "Serialization and Deserialization" in Secure Coding Guidelines for Java SE.
The Java serialization filtering mechanism screens incoming streams of serialized objects to help improve security and robustness. Filters can validate incoming instances of classes before they are deserialized.
As stated in JEP 290 and JEP 415, the goals of the Java serialization filtering mechanism are to:
Provide a way to narrow the classes that can be deserialized down to a context-appropriate set of classes.
Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors.
Allow RMI-exported objects to validate the classes expected in invocations.
There are two kinds of filters:
ObjectInputStream
.You can implement a serialization filter in the following ways:
Specify a JVM-wide, pattern-based filter with the
jdk.serialFilter
property: A pattern-based
filter consists of a sequence of patterns that can accept or
reject the name of specific classes, packages, or modules. It can
place limits on array sizes, graph depth, total references, and
stream size. A typical use case is to add classes that have been
identified as potentially compromising the Java runtime to a
reject-list. If you specify a pattern-based filter with the
jdk.serialFilter
property, then you don't have to
modify your application.
Implement a custom or pattern-based stream-specific
filter with the sun.misc.ObjectInputFilter
API: You
can implement a filter with the sun.misc.ObjectInputFilter
API, which you then set on an ObjectInputStream
with the method ObjectInputFilter.Config.setObjectInputFilter(ObjectInputStream, ObjectInputFilter)
. You
can create a pattern-based filter with the
ObjectInputFilter
API by calling the
Config.createFilter(String)
method.
The sun.misc.ObjectInputFilter
interface is the same as the
ObjectInputFilter
interface in JDK 11 except that it's in the sun.misc
package.
Note: A serialization filter is not enabled
or configured by default. Serialization filtering doesn't occur
unless you have specified the filter in a system property or a
Security Property or set it with the
sun.misc.ObjectInputFilter
class.
For every new object in the stream, the filter mechanism applies only one filter to it. However, this filter might be a combination of filters.
In most cases, a stream-specific filter should check if a
JVM-wide filter is set, especially if you haven't specified a
filter factory. If a JVM-wide filter does exist, then the
stream-specific filter should invoke it and use the JVM-wide
filter's result unless the status is UNDECIDED
.
A filter factory selects, chooses, or combines filters into a
single filter to be used for a stream. When you specify one, a
deserialization operation uses it when it encounters a class for
the first time to determine whether to allow it. (Subsequent
instances of the same class aren't filtered.) It's implemented as
a BinaryOperator<sun.misc.ObjectInputFilter>
and
specified in a system or Security property; see Setting a Filter
Factory. Whenever an ObjectInputStream
is created,
the filter factory selects an ObjectInputFilter
.
However, you can have a different filter created based on the
characteristics of the stream and the filter that the filter
factory previously created.
Allow-lists and reject-lists can be implemented using pattern-based filters or custom filters. These lists allow you to take proactive and defensive approaches to protect your applications.
The proactive approach uses allow-lists to allow only class names that are recognized and trusted and to reject all others. You can implement allow-lists in your code when you develop your application, or later by defining pattern-based filters. If your application only deals with a small set of classes then this approach can work very well. You can implement allow-lists by specifying the names of classes, packages, or modules that are allowed.
The defensive approach uses reject-lists to reject instances
of classes that are not trusted. Usually, reject-lists are
implemented after an attack that reveals that a class is a
problem. A class name can be added to a reject-list, without a
code change, by adding it to a pattern-based filter that's
specified in the jdk.serialFilter
property.
Pattern-based filters are filters that you define without
changing your application code. You add JVM-wide filters in
properties files or application-specific filters on the
java
command line.
A pattern-based filter is a sequence of patterns. Each pattern is matched against the name of a class in the stream or a resource limit. Class-based and resource limit patterns can be combined in one filter string, with each pattern separated by a semicolon (;).
Separate patterns by semicolons. For example:
pattern1.*;pattern2.*
White space is significant and is considered part of the pattern.
Put the limits first in the string. They are evaluated first regardless of where they are in the string, so putting them first reinforces the ordering. Otherwise, patterns are evaluated from left to right.
!
is rejected. A class name that matches a pattern
without !
is allowed. The following filter rejects
pattern1.MyClass
but allows
pattern2.MyClass
:
!pattern1.*;pattern2.*
*
) to represent
unspecified class names in a pattern as shown in the following
examples:
To match every class name, use *
To match every class name in mypackage
, use
mypackage.*
To match every class name in mypackage
and its
subpackages, use mypackage.**
To match every class name that starts with text
,
use text*
If a class name doesn't match any filter, then it is allowed.
If you want to allow only certain class names, then your filter
must reject everything that doesn't match. To reject all class
names other than those specified, include !*
as the
last pattern in a class filter.
For a complete description of the syntax for the patterns, see JEP 290.
The following are some of the limitations of pattern-based filters:
Patterns can't allow different sizes of arrays based on the class name.
Patterns can't match classes based on the supertype or interfaces of the class name.
Patterns have no state and can't make choices depending on the class instances deserialized earlier in the stream.
Note: A pattern-based filter doesn't check interfaces that are implemented by classes being deserialized. The filter is invoked for interfaces explicitly referenced in the stream; it isn't invoked for interfaces implemented by classes for objects being deserialized.
You can define a pattern-based filter as a system property for one application. A system property supersedes a Security Property value.
To create a filter that only applies to one application, and
only to a single invocation of Java, define the
jdk.serialFilter
system property in the command
line.
The following example shows how to limit resource usage for an individual application:
java
-Djdk.serialFilter=maxarray=100000;maxdepth=20;maxrefs=500 com.example.test.Application
You can define a pattern-based, JVM-wide filter that affects
every application run with a Java runtime from
$JAVA_HOME
by specifying it as a Security Property.
(Note that a system property supersedes a Security Property
value.) Edit the file
$JAVA_HOME/lib/security/java.security
and add the
pattern-based filter to the jdk.serialFilter
Security Property.
You can create a pattern-based class filter that is applied globally. For example, the pattern might be a class name or a package with wildcard.
In the following example, the filter rejects one class name from a package (!example.somepackage.SomeClass
), and
allows all other class names in the package:
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;
The previous example filter allows all other class names, not
just those in example.somepackage.*
. To reject all
other class names, add !*
:
jdk.serialFilter=!example.somepackage.SomeClass;example.somepackage.*;!*
Maximum allowed array size. For example:
maxarray=100000;
Maximum depth of a graph. For example:
maxdepth=20;
Maximum references in a graph between objects. For example:
maxrefs=500;
Maximum number of bytes in a stream. For example:
maxbytes=500000;
Custom filters are filters you specify in your application's code. They are set on an individual stream or on all streams in a process. You can implement a custom filter as a pattern, a method, a lambda expression, or a class.
You can set a custom filter on one
ObjectInputStream
, or, to apply the same filter to
every stream, set a JVM-wide filter. If an
ObjectInputStream
doesn't have a filter defined for
it, the JVM-wide filter is called, if there is one.
While the stream is being decoded, the following actions occur:
java.lang.String
instances that are encoded
concretely in the stream.Unless a filter rejects the object, the object is accepted.
You can set a filter on an individual
ObjectInputStream
when the input to the stream is
untrusted and the filter has a limited set of classes or
constraints to enforce. For example, you could ensure that a
stream only contains numbers, strings, and other
application-specified types.
A custom filter is set using the
sun.misc.ObjectInputFilter.Config.setObjectInputFilter(ObjectInputStream, ObjectInputFilter)
method. The custom filter must
be set before objects are read from the stream.
In the following example, the
setObjectInputFilter
method is invoked with the
dateTimeFilter
method. This filter only accepts
classes from the java.time
package. The
dateTimeFilter
method is defined in a code sample in
Setting a Custom Filter as a Method.
LocalDateTime readDateTime(InputStream is) throws IOException { try (ObjectInputStream ois = new ObjectInputStream(is)) { ObjectInputFilter.Config.setObjectInputFilter(ois, FilterClass::dateTimeFilter); return (LocalDateTime) ois.readObject(); } catch (ClassNotFoundException ex) { IOException ioe = new StreamCorruptedException("class missing"); ioe.initCause(ex); throw ioe; } }
You can set a JVM-wide filter that applies to every use of
ObjectInputStream
unless it is overridden on a
specific stream. If you can identify every type and condition
that is needed by the entire application, the filter can allow
those and reject the rest. Typically, JVM-wide filters are used
to reject specific classes or packages, or to limit array sizes,
graph depth, or total graph size.
A JVM-wide filter is set once using the methods of the
sun.misc.ObjectInputFilter.Config
class. The filter can be an
instance of a class, a lambda expression, a method reference, or
a pattern.
ObjectInputFilter filter = ... ObjectInputFilter.Config.setSerialFilter(filter);
In the following example, the JVM-wide filter is set by using a lambda expression.
ObjectInputFilter.Config.setSerialFilter( info -> info.depth() > 10 ? Status.REJECTED : Status.UNDECIDED);
In the following example, the JVM-wide filter is set by using a method reference:
ObjectInputFilter.Config.setSerialFilter(FilterClass::dateTimeFilter);
A pattern-based custom filter, which is convenient for simple
cases, can be created by using the
sun.misc.ObjectInputFilter.Config.createFilter
method. You
can create a pattern-based filter as a system property or
Security Property. Implementing a pattern-based filter as a
method or a lambda expression gives you more flexibility.
The filter patterns can accept or reject specific names of classes, packages, and modules and can place limits on array sizes, graph depth, total references, and stream size. Patterns cannot match the names of the supertype or interfaces of the class.
In the following example, the filter allowsexample.File
and rejects
example.Directory
.
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("example.File;!example.Directory");
This example allows only example.File
. All other
class names are rejected.
ObjectInputFilter filesOnlyFilter = ObjectInputFilter.Config.createFilter("example.File;!*");
A custom filter can be implemented as a class implementing the
sun.misc.ObjectInputFilter
interface, as a lambda
expression, or as a method.
A filter is typically stateless and performs checks solely on
the input parameters. However, you may implement a filter that,
for example, maintains state between calls to the
checkInput
method to count artifacts in the
stream.
In the following example, the FilterNumber
class
allows any object that is an instance of the Number
class and rejects all others.
class FilterNumber implements ObjectInputFilter { public Status checkInput(FilterInfo filterInfo) { Class<?> clazz = filterInfo.serialClass(); if (clazz != null) { return (Number.class.isAssignableFrom(clazz)) ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED; } return ObjectInputFilter.Status.UNDECIDED; } }
In the example:
checkInput
method accepts an
ObjectInputFilter.FilterInfo
object. The object's
methods provide access to the class to be checked, array size,
current depth, number of references to existing objects, and
stream size read so far.serialClass
is not null, then the value is
checked to see if the class of the object is Number
.
If so, it is accepted and returns
ObjectInputFilter.Status.ALLOWED
. Otherwise, it is
rejected and returns
ObjectInputFilter.Status.REJECTED
.ObjectInputFilter.Status.UNDECIDED
. Deserialization
continues, and any remaining filters are run until the object is
accepted or rejected. If there are no other filters, the object
is accepted.A custom filter can also be implemented as a method. The method reference is used instead of an inline lambda expression.
The dateTimeFilter
method that is defined in the
following example is used by the code sample in Setting a Custom Filter for an Individual Stream.
public class FilterClass { static ObjectInputFilter.Status dateTimeFilter(ObjectInputFilter.FilterInfo info) { Class<?> serialClass = info.serialClass(); if (serialClass != null) { return serialClass.getPackageName().equals("java.time") ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED; } return ObjectInputFilter.Status.UNDECIDED; } }
This custom filter allows only the classes found in the base module of the JDK:
static ObjectInputFilter.Status baseFilter(ObjectInputFilter.FilterInfo info) { Class<?> serialClass = info.serialClass(); if (serialClass != null) { return serialClass.getModule().getName().equals("java.base") ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED; } return ObjectInputFilter.Status.UNDECIDED; }
A filter factory is a BinaryOperator
, which is a
function of two operands that chooses the filter for a stream.
You set a filter factory by specifying its class name in the
system property jdk.serialFilterFactory
or in the
Security Property jdk.serialFilterFactory
.
The sample code BasicFilterFactory.java
is a simple example of a filter factory. It
prints its sun.misc.ObjectInputFilter
parameters every time
its apply
method is invoked, then returns an
ObjectInputFilter
that consists of the
apply
method's parameters merged into one
filter.
When you set a filter factory, the filter factory's method
BinaryOperator<ObjectInputFilter>.apply(sun.misc.ObjectInputFilter
t, sun.misc.ObjectInputFilter u)
will be invoked when an
ObjectInputStream
is constructed and when a
stream-specific filter is set on an
ObjectInputStream
. The parameter t
is
the current filter and u
is the requested filter.
When apply
is first invoked, t
will be
null. If a JVM-wide filter has been set, then when
apply
is first invoked, u
will be the
JVM-wide filter. Otherwise, u
will be null. The
apply
method (which you must implement yourself)
returns the filter to be used for the stream. If
apply
is invoked again, then the parameter
t
will be this returned filter. When you set a
filter with the method
sun.misc.ObjectInputFilter.Config.setObjectInputFilter(ObjectInputStrea, ObjectInputFilter)
,
then parameter u
will be this filter.
Note: To protect against unexpected deserializations, ensure that security experts thoroughly review how your filter factories select and combine filters.
You can set a filter factory that applies to only one
application and to only a single invocation of Java by specifying
it in the jdk.serialFilterFactory
system property in
the command line:
java
-Djdk.serialFilterFactory=FilterFactoryClassName
YourApplication
The value of jdk.serialFilterFactory
is the fully
qualified 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 (which the method
java.lang.ClassLoader.getSystemClassLoader()
returns).
You can set a JVM-wide filter factory that affects every
application run with a Java runtime from $JAVA_HOME
by specifying it in a Security Property. Note that a system
property supersedes a Security Property value. Edit the file
$JAVA_HOME/lib/security/java.security
and specify
the filter factory's class name in the
jdk.serialFilterFactory
Security Property.
The sample code TestBasicFilter.java
demonstrates the filter factory
BasicFilterFactory
.
Run TestBasicFilter
with the following
command:
java -Djdk.serialFilterFactory=BasicFilterFactory
TestBasicFilter
This command prints output similar to the following:
Current filter: null Requested filter: example.*;java.lang.*;!* Current filter: example.*;java.lang.*;!* Requested filter: TestBasicFilter$FilterNumber@7cca494b Read obj: 42
The apply
method is invoked twice: when the
ObjectInputStream
ois
is created and
when the method ObjectInputFilter.Config.setObjectInputFilter(ObjectInputStream, ObjectInputFilter)
is called.
Note: You can set a filter on an
ObjectInputStream
only once. An
IllegalStateException
will be thrown otherwise.
The Java Remote Method Invocation (RMI) Registry, the RMI Distributed Garbage Collector, and Java Management Extensions (JMX) all have filters that are included in the JDK. You should specify your own filters for the RMI Registry and the RMI Distributed Garbage Collector to add additional protection.
Note: Use these built-in filters as starting
points only. Edit the
sun.rmi.registry.registryFilter
system property to
configure reject-lists and/or extend the allow-list to add
additional protection for the RMI Registry. To protect the whole
application, add the patterns to the
jdk.serialFilter
global system property to increase
protection for other serialization users that do not have their
own custom filters.
The RMI Registry has a built-in allow-list filter that allows
objects to be bound in the registry. It includes instances of the
java.rmi.Remote
, java.lang.Number
,
java.lang.reflect.Proxy
,
java.rmi.server.UnicastRef
,
java.rmi.server.UID
,
java.rmi.server.RMIClientSocketFactory
, and
java.rmi.server.RMIServerSocketFactory
classes.
maxarray=1000000;maxdepth=20
Supersede the built-in filter by defining a filter using the
sun.rmi.registry.registryFilter
system property with
a pattern. If the filter that you define either accepts classes
passed to the filter, or rejects classes or sizes, the built-in
filter is not invoked. If your filter does not accept or reject
anything, the built-filter is invoked.
Note: Use these built-in filters as starting
points only. Edit the sun.rmi.transport.dgcFilter
system property to configure reject-lists and/or extend the
allow-list to add additional protection for Distributed Garbage
Collector. To protect the whole application, add the patterns to
the jdk.serialFilter
global system property to
increase protection for other serialization users that do not
have their own custom filters.
The RMI Distributed Garbage Collector has a built-in
allow-list filter that accepts a limited set of classes. It
includes instances of the java.rmi.server.ObjID
,
java.rmi.server.UID
, java.rmi.dgc.VMID
,
and java.rmi.dgc.Lease
classes.
maxarray=1000000;maxdepth=20
Supersede the built-in filter by defining a filter using the
sun.rmi.transport.dgcFilter
system property with a
pattern. If the filter accepts classes passed to the filter, or
rejects classes or sizes, the built-in filter is not invoked. If
the superseding filter does not accept or reject anything, the
built-filter is invoked.
Note: Use these built-in filters as starting
points only. Edit the
jmx.remote.rmi.server.serial.filter.pattern
management property to configure reject-lists and/or extend the
allow-list to add additional protection for JMX. To protect the
whole application, add the patterns to the
jdk.serialFilter
global system property to increase
protection for other serialization users that do not have their
own custom filters.
JMX has a built-in filter to limit a set of classes allowed to
be sent as a deserializing parameters over RMI to the server.
That filter is disabled by default. To enable the filter, define
the jmx.remote.rmi.server.serial.filter.pattern
management property with a pattern.
The pattern must include the types that are allowed to be sent
as parameters over RMI to the server and all types they depends
on, plus javax.management.ObjectName
and
java.rmi.MarshalledObject
types. For example, to
limit the allowed set of classes to Open MBean types and the
types they depend on, add the following line to
management.properties
file.
com.sun.management.jmxremote.serial.filter.pattern=java.lang.*;java.math.BigInteger;java.math.BigDecimal;java.util.*;javax.management.openmbean.*;javax.management.ObjectName;java.rmi.MarshalledObject;!*
You can turn on logging to record the initialization, rejections, and acceptances of calls to serialization filters. Use the log output as a diagnostic tool to see what's being deserialized, and to confirm your settings when you configure allow-lists and reject-lists.
When logging is enabled, filter actions are logged to the
java.io.serialization
logger.
To enable serialization filter logging, edit the
$JDK_HOME/conf/logging.properties
file.
To log calls that are rejected, add
java.io.serialization.level = FINE
To log all filter results, add
java.io.serialization.level = FINEST