8264859: Implement Context-Specific Deserialization Filters

Reviewed-by: bchristi, dfuchs, chegar
This commit is contained in:
Roger Riggs 2021-06-09 12:30:07 +00:00
parent dd34a4c28d
commit 13d6180421
9 changed files with 2556 additions and 187 deletions

File diff suppressed because it is too large Load Diff

View File

@ -25,6 +25,7 @@
package java.io;
import java.io.ObjectInputFilter.Config;
import java.io.ObjectStreamClass.WeakClassKey;
import java.io.ObjectStreamClass.RecordSupport;
import java.lang.System.Logger;
@ -66,6 +67,23 @@ import sun.security.action.GetIntegerAction;
* practices for defensive use of serial filters.
* </strong></p>
*
* <p>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.
* {@link ObjectInputFilter} describes how to use filters and
* {@link ObjectInputFilter.Config} describes how to configure the filter and filter factory.
* Each stream has an optional deserialization filter
* to check the classes and resource limits during deserialization.
* The JVM-wide filter factory ensures that a filter can be set on every {@link ObjectInputStream}
* and every object read from the stream can be checked.
* The {@linkplain #ObjectInputStream() ObjectInputStream constructors} invoke the filter factory
* to select the initial filter which may be updated or replaced by {@link #setObjectInputFilter}.
* <p>
* If an ObjectInputStream has a filter, the {@link ObjectInputFilter} can check that
* the classes, array lengths, number of references in the stream, depth, and
* number of bytes consumed from the input stream are allowed and
* if not, can terminate deserialization.
*
* <p>ObjectOutputStream and ObjectInputStream can provide an application with
* persistent storage for graphs of objects when used with a FileOutputStream
* and FileInputStream respectively. ObjectInputStream is used to recover
@ -188,16 +206,6 @@ import sun.security.action.GetIntegerAction;
* protected) or that there are get and set methods that can be used to restore
* the state.
*
* <p>The contents of the stream can be filtered during deserialization.
* If a {@linkplain #setObjectInputFilter(ObjectInputFilter) filter is set}
* on an ObjectInputStream, the {@link ObjectInputFilter} can check that
* the classes, array lengths, number of references in the stream, depth, and
* number of bytes consumed from the input stream are allowed and
* if not, can terminate deserialization.
* A {@linkplain ObjectInputFilter.Config#setSerialFilter(ObjectInputFilter) system-wide filter}
* can be configured that is applied to each {@code ObjectInputStream} unless replaced
* using {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter}.
*
* <p>Any exception that occurs while deserializing an object will be caught by
* the ObjectInputStream and abort the reading process.
*
@ -347,14 +355,20 @@ public class ObjectInputStream
*/
private ObjectInputFilter serialFilter;
/**
* True if the stream-specific filter has been set; initially false.
*/
private boolean streamFilterSet;
/**
* Creates an ObjectInputStream that reads from the specified InputStream.
* A serialization stream header is read from the stream and verified.
* This constructor will block until the corresponding ObjectOutputStream
* has written and flushed the header.
*
* <p>The serialization filter is initialized to the value of
* {@linkplain ObjectInputFilter.Config#getSerialFilter() the system-wide filter}.
* <p>The constructor initializes the deserialization filter to the filter returned
* by invoking the {@link Config#getSerialFilterFactory()} with {@code null} for the current filter
* and the {@linkplain Config#getSerialFilter() static JVM-wide filter} for the requested filter.
*
* <p>If a security manager is installed, this constructor will check for
* the "enableSubclassImplementation" SerializablePermission when invoked
@ -377,7 +391,8 @@ public class ObjectInputStream
bin = new BlockDataInputStream(in);
handles = new HandleTable(10);
vlist = new ValidationList();
serialFilter = ObjectInputFilter.Config.getSerialFilter();
streamFilterSet = false;
serialFilter = Config.getSerialFilterFactorySingleton().apply(null, Config.getSerialFilter());
enableOverride = false;
readStreamHeader();
bin.setBlockDataMode(true);
@ -388,8 +403,9 @@ public class ObjectInputStream
* ObjectInputStream to not have to allocate private data just used by this
* implementation of ObjectInputStream.
*
* <p>The serialization filter is initialized to the value of
* {@linkplain ObjectInputFilter.Config#getSerialFilter() the system-wide filter}.
* <p>The constructor initializes the deserialization filter to the filter returned
* by invoking the {@link Config#getSerialFilterFactory()} with {@code null} for the current filter
* and the {@linkplain Config#getSerialFilter() static JVM-wide filter} for the requested filter.
*
* <p>If there is a security manager installed, this method first calls the
* security manager's {@code checkPermission} method with the
@ -412,7 +428,8 @@ public class ObjectInputStream
bin = null;
handles = null;
vlist = null;
serialFilter = ObjectInputFilter.Config.getSerialFilter();
streamFilterSet = false;
serialFilter = Config.getSerialFilterFactorySingleton().apply(null, Config.getSerialFilter());
enableOverride = true;
}
@ -431,7 +448,7 @@ public class ObjectInputStream
* priorities. The callbacks are registered by objects (in the readObject
* special methods) as they are individually restored.
*
* <p>The serialization filter, when not {@code null}, is invoked for
* <p>The deserialization filter, when not {@code null}, is invoked for
* each object (regular or class) read to reconstruct the root object.
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
*
@ -443,7 +460,7 @@ public class ObjectInputStream
* @throws ClassNotFoundException Class of a serialized object cannot be
* found.
* @throws InvalidClassException Something is wrong with a class used by
* serialization.
* deserialization.
* @throws StreamCorruptedException Control information in the
* stream is inconsistent.
* @throws OptionalDataException Primitive data was found in the
@ -564,7 +581,7 @@ public class ObjectInputStream
* invocation of readObject or readUnshared on the ObjectInputStream,
* even if the underlying data stream has been manipulated.
*
* <p>The serialization filter, when not {@code null}, is invoked for
* <p>The deserialization filter, when not {@code null}, is invoked for
* each object (regular or class) read to reconstruct the root object.
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
*
@ -872,7 +889,7 @@ public class ObjectInputStream
* <p>When a subclass is replacing objects it must insure that the
* substituted object is compatible with every field where the reference
* will be stored. Objects whose type is not a subclass of the type of the
* field or array element abort the serialization by raising an exception
* field or array element abort the deserialization by raising an exception
* and the object is not be stored.
*
* <p>This method is called only once when each object is first
@ -1223,13 +1240,13 @@ public class ObjectInputStream
}
/**
* Returns the serialization filter for this stream.
* The serialization filter is the most recent filter set in
* {@link #setObjectInputFilter setObjectInputFilter} or
* the initial system-wide filter from
* {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}.
* Returns the deserialization filter for this stream.
* The filter is the result of invoking the
* {@link Config#getSerialFilterFactory() JVM-wide filter factory}
* either by the {@linkplain #ObjectInputStream() constructor} or the most recent invocation of
* {@link #setObjectInputFilter setObjectInputFilter}.
*
* @return the serialization filter for the stream; may be null
* @return the deserialization filter for the stream; may be null
* @since 9
*/
public final ObjectInputFilter getObjectInputFilter() {
@ -1237,8 +1254,23 @@ public class ObjectInputStream
}
/**
* Set the serialization filter for the stream.
* The filter's {@link ObjectInputFilter#checkInput checkInput} method is called
* Set the deserialization filter for the stream.
*
* The deserialization filter is set to the filter returned by invoking the
* {@linkplain Config#getSerialFilterFactory() JVM-wide filter factory}
* with the {@linkplain #getObjectInputFilter() current filter} and the {@code filter} parameter.
* The current filter was set in the
* {@linkplain #ObjectInputStream() ObjectInputStream constructors} by invoking the
* {@linkplain Config#getSerialFilterFactory() JVM-wide filter factory} and may be {@code null}.
* {@linkplain #setObjectInputFilter(ObjectInputFilter)} This method} can be called
* once and only once before reading any objects from the stream;
* for example, by calling {@link #readObject} or {@link #readUnshared}.
*
* <p>It is not permitted to replace a {@code non-null} filter with a {@code null} filter.
* If the {@linkplain #getObjectInputFilter() current filter} is {@code non-null},
* the value returned from the filter factory must be {@code non-null}.
*
* <p>The filter's {@link ObjectInputFilter#checkInput checkInput} method is called
* for each class and reference in the stream.
* The filter can check any or all of the class, the array length, the number
* of references, the depth of the graph, and the size of the input stream.
@ -1247,21 +1279,14 @@ public class ObjectInputStream
* and the current object being deserialized.
* The number of references is the cumulative number of objects and references
* to objects already read from the stream including the current object being read.
* The filter is invoked only when reading objects from the stream and for
* not primitives.
* The filter is invoked only when reading objects from the stream and not for
* primitives.
* <p>
* If the filter returns {@link ObjectInputFilter.Status#REJECTED Status.REJECTED},
* {@code null} or throws a {@link RuntimeException},
* the active {@code readObject} or {@code readUnshared}
* throws {@link InvalidClassException}, otherwise deserialization
* continues uninterrupted.
* <p>
* The serialization filter is initialized to the value of
* {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}
* when the {@code ObjectInputStream} is constructed and can be set
* to a custom filter only once.
* The filter must be set before reading any objects from the stream;
* for example, by calling {@link #readObject} or {@link #readUnshared}.
*
* @implSpec
* The filter, when not {@code null}, is invoked during {@link #readObject readObject}
@ -1303,9 +1328,10 @@ public class ObjectInputStream
* @param filter the filter, may be null
* @throws SecurityException if there is security manager and the
* {@code SerializablePermission("serialFilter")} is not granted
* @throws IllegalStateException if the {@linkplain #getObjectInputFilter() current filter}
* is not {@code null} and is not the system-wide filter, or
* if an object has been read
* @throws IllegalStateException if an object has been read,
* if the filter factory returns {@code null} when the
* {@linkplain #getObjectInputFilter() current filter} is non-null, or
* if the filter has already been set.
* @since 9
*/
public final void setObjectInputFilter(ObjectInputFilter filter) {
@ -1314,20 +1340,25 @@ public class ObjectInputStream
if (sm != null) {
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
}
// Allow replacement of the system-wide filter if not already set
if (serialFilter != null &&
serialFilter != ObjectInputFilter.Config.getSerialFilter()) {
throw new IllegalStateException("filter can not be set more than once");
}
if (totalObjectRefs > 0 && !Caches.SET_FILTER_AFTER_READ) {
throw new IllegalStateException(
"filter can not be set after an object has been read");
}
this.serialFilter = filter;
if (streamFilterSet) {
throw new IllegalStateException("filter can not be set more than once");
}
streamFilterSet = true;
// Delegate to serialFilterFactory to compute stream filter
ObjectInputFilter next = Config.getSerialFilterFactory()
.apply(serialFilter, filter);
if (serialFilter != null && next == null) {
throw new IllegalStateException("filter can not be replaced with null filter");
}
serialFilter = next;
}
/**
* Invokes the serialization filter if non-null.
* Invokes the deserialization filter if non-null.
*
* If the filter rejects or an exception is thrown, throws InvalidClassException.
*

View File

@ -47,6 +47,7 @@ public final class StaticProperty {
private static final String JAVA_LIBRARY_PATH;
private static final String SUN_BOOT_LIBRARY_PATH;
private static final String JDK_SERIAL_FILTER;
private static final String JDK_SERIAL_FILTER_FACTORY;
private static final String JAVA_IO_TMPDIR;
private static final String NATIVE_ENCODING;
@ -62,6 +63,7 @@ public final class StaticProperty {
JAVA_LIBRARY_PATH = getProperty(props, "java.library.path", "");
SUN_BOOT_LIBRARY_PATH = getProperty(props, "sun.boot.library.path", "");
JDK_SERIAL_FILTER = getProperty(props, "jdk.serialFilter", null);
JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null);
NATIVE_ENCODING = getProperty(props, "native.encoding");
}
@ -184,6 +186,20 @@ public final class StaticProperty {
return JDK_SERIAL_FILTER;
}
/**
* Return the {@code jdk.serialFilterFactory} system property.
*
* <strong>{@link SecurityManager#checkPropertyAccess} is NOT checked
* in this method. The caller of this method should take care to ensure
* that the returned property is not made accessible to untrusted code.</strong>
*
* @return the {@code user.name} system property
*/
public static String jdkSerialFilterFactory() {
return JDK_SERIAL_FILTER_FACTORY;
}
/**
* Return the {@code native.encoding} system property.
*

View File

@ -979,11 +979,29 @@ jdk.xml.dsig.secureValidationPolicy=\
noDuplicateIds,\
noRetrievalMethodLoops
#
# Serialization system-wide filter
# Deserialization system-wide filter factory
#
# A filter, if configured, is used by java.io.ObjectInputStream during
# deserialization to check the contents of the stream.
# A filter factory class name is used to configure the system-wide filter factory.
# The filter factory value "OVERRIDE" in combination with setting "jdk.serialFilter"
# indicates that the builtin filter factory can be overridden by the application.
# The class must be public, must have a public zero-argument constructor, implement the
# java.util.stream.BinaryOperator<ObjectInputFilter> interface, provide its implementation and
# be accessible via the application class loader.
# A builtin filter factory is used if no filter factory is defined.
# See java.io.ObjectInputFilter.Config for more information.
#
# If the system property jdk.serialFilterFactory is also specified, it supersedes
# the security property value defined here.
#
#jdk.serialFilterFactory=<classname>
#
# Deserialization system-wide filter
#
# A filter, if configured, is used by the filter factory to provide the filter used by
# java.io.ObjectInputStream during deserialization to check the contents of the stream.
# A filter is configured as a sequence of patterns, each pattern is either
# matched against the name of a class in the stream or defines a limit.
# Patterns are separated by ";" (semicolon).

View File

@ -0,0 +1,887 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputFilter.Status;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
import java.util.ArrayDeque;
import java.util.List;
import java.util.Objects;
import java.util.function.BinaryOperator;
import java.util.function.Predicate;
import static java.io.ObjectInputFilter.Status.ALLOWED;
import static java.io.ObjectInputFilter.Status.REJECTED;
import static java.io.ObjectInputFilter.Status.UNDECIDED;
/* @test
* @run testng/othervm -Djdk.serialFilterTrace=true SerialFactoryExample
* @run testng/othervm -Djdk.serialFilterFactory=SerialFactoryExample$FilterInThread -Djdk.serialFilterTrace=true SerialFactoryExample
* @summary Test SerialFactoryExample
*/
/*
* Context-specific Deserialization Filter Example
*
* To protect deserialization of a thread or a call to an untrusted library function,
* a filter is set that applies to every deserialization within the thread.
*
* The `doWithSerialFilter` method arguments are a serial filter and
* a lambda to invoke with the filter in force. Its implementation creates a stack of filters
* using a `ThreadLocal`. The stack of filters is composed with the static JVM-wide filter,
* and an optional stream-specific filter.
*
* The FilterInThread filter factory is set as the JVM-wide filter factory.
* When the filter factory is invoked during the construction of each `ObjectInputStream`,
* it retrieves the filter(s) from the thread local and combines it with the static JVM-wide filter,
* and the stream-specific filter.
*
* If more than one filter is to be applied to the stream, two filters can be composed
* using `ObjectInputFilter.merge`. When invoked, each of the filters is invoked and the results
* are combined 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.
* Hierarchies and chains of filters can be built using `ObjectInputFilter.merge`.
*
* The `doWithSerialFilter` calls can be nested. When nested, the filters are concatenated.
*/
@Test
public class SerialFactoryExample {
@DataProvider(name = "Examples")
static Object[][] examples() {
return new Object[][]{
{new Point(1, 2), null,
ALLOWED},
{new Point(1, 2), ObjectInputFilter.Config.createFilter("SerialFactoryExample$Point"),
ALLOWED},
{Integer.valueOf(10), Filters.allowPlatformClasses(),
ALLOWED}, // Integer is a platform class
{new int[10], ObjectInputFilter.Config.createFilter("SerialFactoryExample$Point"),
UNDECIDED}, // arrays of primitives are UNDECIDED -> allowed
{int.class, ObjectInputFilter.Config.createFilter("SerialFactoryExample$Point"),
UNDECIDED}, // primitive classes are UNDECIDED -> allowed
{new Point[] {new Point(1, 1)}, ObjectInputFilter.Config.createFilter("SerialFactoryExample$Point"),
ALLOWED}, // Arrays of allowed classes are allowed
{new Integer[10], ObjectInputFilter.Config.createFilter("SerialFactoryExample$Point"),
REJECTED}, // Base component type is checked -> REJECTED
{new Point(1, 2), ObjectInputFilter.Config.createFilter("!SerialFactoryExample$Point"),
REJECTED}, // Denied
{new Point(1, 3), Filters.allowPlatformClasses(),
REJECTED}, // Not a platform class
{new Point(1, 4), ObjectInputFilter.Config.createFilter("java.lang.Integer"),
REJECTED}, // Only Integer is ALLOWED
{new Point(1, 5), ObjectInputFilter.allowFilter(cl -> cl.getClassLoader() == ClassLoader.getPlatformClassLoader(), UNDECIDED),
REJECTED}, // Not platform loader is UNDECIDED -> a class that should not be undecided -> rejected
};
}
@Test(dataProvider = "Examples")
void examples(Serializable obj, ObjectInputFilter filter, Status expected) {
// Establish FilterInThread as the application-wide filter factory
FilterInThread filterInThread;
if (ObjectInputFilter.Config.getSerialFilterFactory() instanceof FilterInThread fit) {
// Filter factory selected on the command line with -Djdk.serialFilterFactory=<classname>
filterInThread = fit;
} else {
// Create a FilterInThread filter factory and set
// An IllegalStateException will be thrown if the filter factory was already
// initialized to an incompatible filter factory.
filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);
}
try {
filterInThread.doWithSerialFilter(filter, () -> {
byte[] bytes = writeObject(obj);
Object o = deserializeObject(bytes);
});
if (expected.equals(REJECTED))
Assert.fail("IllegalClassException should have occurred");
} catch (UncheckedIOException uioe) {
IOException ioe = uioe.getCause();
Assert.assertEquals(ioe.getClass(), InvalidClassException.class, "Wrong exception");
Assert.assertEquals(REJECTED, expected, "Exception should not have occurred");
}
}
/**
* Test various filters with various objects and the resulting status
* @param obj an object
* @param filter a filter
* @param expected status
*/
@Test(dataProvider = "Examples")
void checkStatus(Serializable obj, ObjectInputFilter filter, Status expected) {
// Establish FilterInThread as the application-wide filter factory
FilterInThread filterInThread;
if (ObjectInputFilter.Config.getSerialFilterFactory() instanceof FilterInThread fit) {
// Filter factory selected on the command line with -Djdk.serialFilterFactory=<classname>
filterInThread = fit;
} else {
// Create a FilterInThread filter factory and set
// An IllegalStateException will be thrown if the filter factory was already
// initialized to an incompatible filter factory.
filterInThread = new FilterInThread();
ObjectInputFilter.Config.setSerialFilterFactory(filterInThread);
}
try {
filterInThread.doWithSerialFilter(filter, () -> {
// Classes are serialized as themselves, otherwise pass the object's class
Class<?> clazz = (obj instanceof Class<?>) ? (Class<?>)obj : obj.getClass();
ObjectInputFilter.FilterInfo info = new SerialInfo(clazz);
var compositeFilter = filterInThread.apply(null, ObjectInputFilter.Config.getSerialFilter());
System.out.println(" filter in effect: " + filterInThread.currFilter);
if (compositeFilter != null) {
Status actualStatus = compositeFilter.checkInput(info);
Assert.assertEquals(actualStatus, expected, "Wrong Status");
}
});
} catch (Exception ex) {
Assert.fail("unexpected exception", ex);
}
}
/**
* A Context-specific Deserialization Filter Factory to create filters that apply
* a serial filter to all of the deserializations performed in a thread.
* The purpose is to establish a deserialization filter that will reject all classes
* that are not explicitly included.
* <p>
* The filter factory creates a composite filter of the stream-specific filter,
* the thread-specific filter, the static JVM-wide filter, and a filter to reject all UNDECIDED cases.
* The static JVM-wide filter is always included, if it is configured;
* see ObjectInputFilter.Config.getSerialFilter().
* <p>
* To enable these protections the FilterInThread instance should be set as the
* JVM-wide filter factory in ObjectInputFilter.Config.setSerialFilterFactory.
*
* The {@code doWithSerialFilter} is invoked with a serial filter and a lambda
* to be invoked after the filter is applied.
*/
public static final class FilterInThread
implements BinaryOperator<ObjectInputFilter> {
// ThreadLocal holding the Deque of serial filters to be applied, not null
private final ThreadLocal<ArrayDeque<ObjectInputFilter>> filterThreadLocal =
ThreadLocal.withInitial(() -> new ArrayDeque<>());
private ObjectInputFilter currFilter;
/**
* Construct a FilterInThread deserialization filter factory.
* The constructor is public so FilterInThread can be set on the command line
* with {@code -Djdk.serialFilterFactory=SerialFactoryExample$FilterInThread}.
*/
public FilterInThread() {
}
/**
* Applies the filter to the thread and invokes the runnable.
* The filter is pushed to a ThreadLocal, saving the old value.
* If there was a previous thread filter, the new filter is appended
* and made the active filter.
* The runnable is invoked.
* The previous filter is restored to the ThreadLocal.
*
* @param filter the serial filter to apply
* @param runnable a runnable to invoke
*/
public void doWithSerialFilter(ObjectInputFilter filter, Runnable runnable) {
var prevFilters = filterThreadLocal.get();
try {
if (filter != null)
prevFilters.addLast(filter);
runnable.run();
} finally {
if (filter != null) {
var lastFilter = prevFilters.removeLast();
assert lastFilter == filter : "Filter removed out of order";
}
}
}
/**
* Returns a composite filter of the stream-specific filter, the thread-specific filter,
* the static JVM-wide filter, and a filter to reject all UNDECIDED cases.
* The purpose is to establish a deserialization filter that will reject all classes
* that are not explicitly included.
* The static JVM-wide filter is always checked, if it is configured;
* see ObjectInputFilter.Config.getSerialFilter().
* Any or all of the filters are optional and if not supplied or configured are null.
* <p>
* This method is first called from the constructor with current == null and
* next == static JVM-wide filter.
* The filter returned is the static JVM-wide filter merged with the thread-specific filter
* and followed by a filter to map all UNDECIDED status values to REJECTED.
* This last step ensures that the collective group of filters covers every possible case,
* any classes that are not ALLOWED will be REJECTED.
* <p>
* The method may be called a second time from {@code ObjectInputStream.setObjectInputFilter(next)}
* to add a stream-specific filter. The stream-specific filter is prepended to the
* composite filter created above when called from the constructor.
* <p>
*
* @param curr the current filter, may be null
* @param next the next filter, may be null
* @return a deserialization filter to use for the stream, may be null
*/
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
if (curr == null) {
// Called from the OIS constructor or perhaps OIS.setObjectInputFilter with no current filter
// no current filter, prepend next to threadFilter, both may be null or non-null
// Assemble the filters in sequence, most recently added first
var filters = filterThreadLocal.get();
ObjectInputFilter filter = null;
for (ObjectInputFilter f : filters) {
filter = ObjectInputFilter.merge(f, filter);
}
if (next != null) {
// Prepend a filter to reject all UNDECIDED results
if (filter != null) {
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
// Prepend the next filter to the thread filter, if any
// Initially this would be the static JVM-wide filter passed from the OIS constructor
// The static JVM-wide filter allow, reject, or leave classes undecided
filter = ObjectInputFilter.merge(next, filter);
}
// Check that the static JVM-wide filter did not leave any classes undecided
if (filter != null) {
// Append the filter to reject all UNDECIDED results
filter = ObjectInputFilter.rejectUndecidedClass(filter);
}
// Return the filter, unless a stream-specific filter is set later
// The filter may be null if no filters are configured
currFilter = filter;
return currFilter;
} else {
// Called from OIS.setObjectInputFilter with a previously set filter.
// The curr filter already incorporates the thread filter and rejection of undecided status
// Prepend the stream-specific filter or the current filter if no stream-specific filter
currFilter = (next == null) ? curr : ObjectInputFilter.rejectUndecidedClass(ObjectInputFilter.merge(next, curr));
return currFilter;
}
}
public String toString() {
return Objects.toString(currFilter, "none");
}
}
/**
* Simple example code from the ObjectInputFilter Class javadoc.
*/
public static final class SimpleFilterInThread 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 SimpleFilterInThread() {}
/**
* 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 reject all UNDECIDED results
filter = ObjectInputFilter.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.merge(next, filter);
filter = ObjectInputFilter.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.merge(next, curr);
next = ObjectInputFilter.rejectUndecidedClass(next);
return next;
}
return curr;
}
}
/**
* Applies the filter to the thread and invokes 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);
}
}
}
/**
* Write an object and return a byte array with the bytes.
*
* @param object object to serialize
* @return the byte array of the serialized object
* @throws UncheckedIOException if an exception occurs
*/
private static byte[] writeObject(Object object) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(object);
return baos.toByteArray();
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}
/**
* Deserialize an object.
*
* @param bytes an object.
* @throws UncheckedIOException for I/O exceptions and ClassNotFoundException
*/
private static Object deserializeObject(byte[] bytes) {
try {
InputStream is = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(is);
System.out.println(" filter in effect: " + ois.getObjectInputFilter());
return ois.readObject();
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
} catch (ClassNotFoundException cnfe) {
throw new UncheckedIOException(new InvalidClassException(cnfe.getMessage()));
}
}
/**
* ObjectInputFilter utilities to create filters that combine the results of other filters.
*/
public static final class Filters {
/**
* Returns a filter that returns {@code Status.ALLOWED} if the predicate
* on the class is {@code true}.
* The filter returns {@code ALLOWED} or the {@code otherStatus} based on the predicate
* of the {@code non-null} class and {@code UNDECIDED} if the class is {@code null}.
*
* <p>When the filter's {@link ObjectInputFilter#checkInput checkInput(info)} method is invoked,
* the predicate is applied to the {@link ObjectInputFilter.FilterInfo#serialClass() info.serialClass()},
* the return Status is:
* <ul>
* <li>{@link Status#UNDECIDED UNDECIDED}, if the {@code serialClass} is {@code null},</li>
* <li>{@link Status#ALLOWED ALLOWED}, if the predicate on the class returns {@code true},</li>
* <li>Otherwise, return {@code otherStatus}.</li>
* </ul>
* <p>
* Example, to create a filter that will allow any class loaded from the platform classloader.
* <pre><code>
* ObjectInputFilter f = allowFilter(cl -> cl.getClassLoader() == ClassLoader.getPlatformClassLoader()
* || cl.getClassLoader() == null, Status.UNDECIDED);
* </code></pre>
*
* @param predicate a predicate to test a non-null Class, non-null
* @param otherStatus a Status to use if the predicate is {@code false}
* @return a filter than returns {@code ALLOWED} if the predicate on the class returns {@code true},
* otherwise the {@code otherStatus}
* @since 17
*/
public static ObjectInputFilter allowFilter(Predicate<Class<?>> predicate, Status otherStatus) {
Objects.requireNonNull(predicate, "predicate");
Objects.requireNonNull(otherStatus, "otherStatus");
return new PredicateFilter(predicate, ALLOWED, otherStatus);
}
/**
* Returns a filter that returns {@code Status.REJECTED} if the predicate
* on the class is {@code true}.
* The filter returns {@code ALLOWED} or the {@code otherStatus} based on the predicate
* of the {@code non-null} class and {@code UNDECIDED} if the class is {@code null}.
*
* When the filter's {@link ObjectInputFilter#checkInput checkInput(info)} method is invoked,
* the predicate is applied to the {@link ObjectInputFilter.FilterInfo#serialClass() serialClass()},
* the return Status is:
* <ul>
* <li>{@link Status#UNDECIDED UNDECIDED}, if the {@code serialClass} is {@code null},</li>
* <li>{@link Status#REJECTED REJECTED}, if the predicate on the class returns {@code true},</li>
* <li>Otherwise, return {@code otherStatus}.</li>
* </ul>
* <p>
* Example, to create a filter that will reject any class loaded from the application classloader.
* <pre><code>
* ObjectInputFilter f = rejectFilter(cl ->
* cl.getClassLoader() == ClassLoader.ClassLoader.getSystemClassLoader(), Status.UNDECIDED);
* </code></pre>
*
* @param predicate a predicate to test a non-null Class, non-null
* @param otherStatus a Status to use if the predicate is {@code false}
* @return returns a filter that returns {@link Status#REJECTED REJECTED} if the predicate on the class
* returns {@code true}, otherwise {@link Status#UNDECIDED UNDECIDED}
* @since 17
*/
public static ObjectInputFilter rejectFilter(Predicate<Class<?>> predicate, Status otherStatus) {
Objects.requireNonNull(predicate, "predicate");
Objects.requireNonNull(otherStatus, "otherStatus");
return new PredicateFilter(predicate, REJECTED, otherStatus);
}
/**
* Returns a filter that returns {@code Status.ALLOWED} if the check is for limits
* and not checking a class; otherwise {@code Status.UNDECIDED}.
* If the {@link ObjectInputFilter.FilterInfo#serialClass() serialClass()} is {@code null}, the filter returns
* {@code Status.ALLOWED}, otherwise return {@code Status.UNDECIDED}.
* The limit values of {@link ObjectInputFilter.FilterInfo#arrayLength() arrayLength()},
* {@link ObjectInputFilter.FilterInfo#depth() depth()}, {@link ObjectInputFilter.FilterInfo#references() references()},
* and {@link ObjectInputFilter.FilterInfo#streamBytes() streamBytes()} are not checked.
* To place a limit, create a separate filter with limits such as:
* <pre>{@code
* Config.createFilter("maxarray=10000,maxdepth=40");
* }</pre>
*
* When the filter's {@link ObjectInputFilter#checkInput} method is invoked,
* the Status returned is:
* <ul>
* <li>{@link Status#ALLOWED ALLOWED}, if the {@code serialClass} is {@code null},</li>
* <li>Otherwise, return {@link Status#UNDECIDED UNDECIDED}</li>
* </ul>
*
* @return a filter that returns {@code Status.ALLOWED} if the check is for limits
* and not checking a class; otherwise {@code Status.UNDECIDED}
* @since 17
*/
public static ObjectInputFilter allowMaxLimits() {
return new AllowMaxLimitsFilter(ALLOWED, UNDECIDED);
}
/**
* Returns a filter that merges the status of a filter and another filter.
* If the other filter is {@code null}, the filter is returned.
* Otherwise, a filter is returned to merge the pair of {@code non-null} filters.
*
* The filter returned implements the {@link ObjectInputFilter#checkInput(ObjectInputFilter.FilterInfo)} method
* as follows:
* <ul>
* <li>Invoke {@code filter} on the {@code FilterInfo} to get its {@code status};
* <li>Return {@code REJECTED} if the {@code status} is {@code REJECTED};
* <li>Invoke the {@code otherFilter} to get the {@code otherStatus};
* <li>Return {@code REJECTED} if the {@code otherStatus} is {@code REJECTED};
* <li>Return {@code ALLOWED}, if either {@code status} or {@code otherStatus}
* is {@code ALLOWED}, </li>
* <li>Otherwise, return {@code UNDECIDED}</li>
* </ul>
*
* @param filter a filter, non-null
* @param anotherFilter a filter to be merged with the filter, may be {@code null}
* @return an {@link ObjectInputFilter} that merges the status of the filter and another filter
* @since 17
*/
public static ObjectInputFilter merge(ObjectInputFilter filter, ObjectInputFilter anotherFilter) {
Objects.requireNonNull(filter, "filter");
return (anotherFilter == null) ? filter : new MergeFilter(filter, anotherFilter);
}
/**
* Returns a filter that invokes a filter and maps {@code UNDECIDED} to {@code REJECTED}
* for classes, with some exceptions, and otherwise returns the status.
* The filter returned checks that classes not {@code ALLOWED} and not {@code REJECTED} by the filter
* are {@code REJECTED}, if the class is an array and the base component type is not allowed,
* otherwise the result is {@code UNDECIDED}.
*
* <p>
* Object deserialization accepts a class if the filter returns {@code UNDECIDED}.
* Adding a filter to reject undecided results for classes that have not been
* either allowed or rejected can prevent classes from slipping through the filter.
*
* @implSpec
* The filter returned implements the {@link ObjectInputFilter#checkInput(ObjectInputFilter.FilterInfo)} method
* as follows:
* <ul>
* <li>Invoke the filter on the {@code FilterInfo} to get its {@code status};
* <li>Return the {@code status} if the status is {@code REJECTED} or {@code ALLOWED};
* <li>Return {@code UNDECIDED} if the {@code filterInfo.getSerialClass() serialClass}
* is {@code null};
* <li>Determine the base component type if the {@code serialClass} is
* an {@linkplain Class#isArray() array};
* <li>Return {@code UNDECIDED} if the base component type is
* a {@linkplain Class#isPrimitive() primitive class};
* <li>Invoke the filter on the {@code base component type} to get its
* {@code component status};</li>
* <li>Return {@code ALLOWED} if the component status is {@code ALLOWED};
* <li>Otherwise, return {@code REJECTED}.</li>
* </ul>
*
* @param filter a filter, non-null
* @return an {@link ObjectInputFilter} that maps an {@link Status#UNDECIDED}
* status to {@link Status#REJECTED} for classes, otherwise returns the
* filter status
* @since 17
*/
public static ObjectInputFilter rejectUndecidedClass(ObjectInputFilter filter) {
Objects.requireNonNull(filter, "filter");
return new RejectUndecidedFilter(filter);
}
/**
* Returns a filter that allows a class only if the class was loaded by the platform class loader.
* Otherwise, it returns UNDECIDED; leaving the choice to another filter.
* @return a filter that allows a class only if the class was loaded by the platform class loader
*/
public static ObjectInputFilter allowPlatformClasses() {
return new AllowPlatformClassFilter();
}
/**
* An ObjectInputFilter to evaluate a predicate mapping a class to a boolean.
*/
private static class PredicateFilter implements ObjectInputFilter {
private final Predicate<Class<?>> predicate;
private final Status ifTrueStatus;
private final Status ifFalseStatus;
PredicateFilter(Predicate<Class<?>> predicate, Status ifTrueStatus, Status ifFalseStatus) {
this.predicate = predicate;
this.ifTrueStatus = ifTrueStatus;
this.ifFalseStatus = ifFalseStatus;
}
/**
* Apply the predicate to the class being deserialized, if the class is non-null
* and if it returns {@code true}, return the requested status. Otherwise, return UNDECIDED.
*
* @param info the FilterInfo
* @return the status of applying the predicate, otherwise {@code UNDECIDED}
*/
public ObjectInputFilter.Status checkInput(FilterInfo info) {
Class<?> clazz = info.serialClass();
return (clazz != null && predicate.test(clazz)) ? ifTrueStatus : ifFalseStatus;
}
public String toString() {
return "predicate(" + predicate + ")";
}
}
/**
* An ObjectInputFilter to evaluate if a FilterInfo is checking only limits,
* and not classes.
*/
private static class AllowMaxLimitsFilter implements ObjectInputFilter {
private final Status limitCheck;
private final Status classCheck;
AllowMaxLimitsFilter(Status limitCheck, Status classCheck) {
this.limitCheck = limitCheck;
this.classCheck = classCheck;
}
/**
* If the FilterInfo is only checking a limit, return the requested
* status, otherwise the other status.
*
* @param info the FilterInfo
* @return the status of corresponding to serialClass == null or not
*/
public ObjectInputFilter.Status checkInput(FilterInfo info) {
return (info.serialClass() == null) ? limitCheck : classCheck;
}
public String toString() {
return "allowMaxLimits()";
}
}
/**
* An ObjectInputFilter that merges the status of two filters.
*/
private static class MergeFilter implements ObjectInputFilter {
private final ObjectInputFilter first;
private final ObjectInputFilter second;
MergeFilter(ObjectInputFilter first, ObjectInputFilter second) {
this.first = first;
this.second = second;
}
/**
* Returns REJECTED if either of the filters returns REJECTED,
* and ALLOWED if either of the filters returns ALLOWED.
* Returns {@code UNDECIDED} if either filter returns {@code UNDECIDED}.
*
* @param info the FilterInfo
* @return Status.REJECTED if either of the filters returns REJECTED,
* and ALLOWED if either filter returns ALLOWED; otherwise returns
* {@code UNDECIDED} if both filters returned {@code UNDECIDED}
*/
public ObjectInputFilter.Status checkInput(FilterInfo info) {
Status firstStatus = Objects.requireNonNull(first.checkInput(info), "status");
if (REJECTED.equals(firstStatus)) {
return REJECTED;
}
Status secondStatus = Objects.requireNonNull(second.checkInput(info), "other status");
if (REJECTED.equals(secondStatus)) {
return REJECTED;
}
if (ALLOWED.equals(firstStatus) || ALLOWED.equals(secondStatus)) {
return ALLOWED;
}
return UNDECIDED;
}
@Override
public String toString() {
return "merge(" + first + ", " + second + ")";
}
}
/**
* A filter that maps the status {@code UNDECIDED} to {@code REJECTED} when checking a class.
*/
private static class RejectUndecidedFilter implements ObjectInputFilter {
private final ObjectInputFilter filter;
private RejectUndecidedFilter(ObjectInputFilter filter) {
this.filter = Objects.requireNonNull(filter, "filter");
}
/**
* Apply the filter and return the status if not UNDECIDED and checking a class.
* For array classes, re-check the final component type against the filter.
* Make an exception for Primitive classes that are implicitly allowed by the pattern based filter.
* @param info the FilterInfo
* @return the status of applying the filter and checking the class
*/
public ObjectInputFilter.Status checkInput(FilterInfo info) {
Status status = Objects.requireNonNull(filter.checkInput(info), "status");
Class<?> clazz = info.serialClass();
if (clazz == null || !UNDECIDED.equals(status))
return status;
status = REJECTED;
// Find the base component type
while (clazz.isArray()) {
clazz = clazz.getComponentType();
}
if (clazz.isPrimitive()) {
status = UNDECIDED;
} else {
// for non-primitive types; re-filter the base component type
FilterInfo clazzInfo = new SerialInfo(info, clazz);
Status clazzStatus = filter.checkInput(clazzInfo);
status = (ALLOWED.equals(clazzStatus)) ? ALLOWED : REJECTED;
}
return status;
}
public String toString() {
return "rejectUndecidedClass(" + filter + ")";
}
/**
* FilterInfo instance with a specific class and delegating to an existing FilterInfo.
* Nested in the rejectUndecided class.
*/
static class SerialInfo implements ObjectInputFilter.FilterInfo {
private final FilterInfo base;
private final Class<?> clazz;
SerialInfo(FilterInfo base, Class<?> clazz) {
this.base = base;
this.clazz = clazz;
}
@Override
public Class<?> serialClass() {
return clazz;
}
@Override
public long arrayLength() {
return base.arrayLength();
}
@Override
public long depth() {
return base.depth();
}
@Override
public long references() {
return base.references();
}
@Override
public long streamBytes() {
return base.streamBytes();
}
}
}
/**
* An ObjectInputFilter that merges the results of two filters.
*/
private static class MergeManyFilter implements ObjectInputFilter {
private final List<ObjectInputFilter> filters;
private final Status otherStatus;
MergeManyFilter(List<ObjectInputFilter> first, Status otherStatus) {
this.filters = Objects.requireNonNull(first, "filters");
this.otherStatus = Objects.requireNonNull(otherStatus, "otherStatus");
}
/**
* Returns REJECTED if any of the filters returns REJECTED,
* and ALLOWED if any of the filters returns ALLOWED.
* Returns UNDECIDED if there is no class to be checked or all filters return UNDECIDED.
*
* @param info the FilterInfo
* @return Status.UNDECIDED if there is no class to check,
* Status.REJECTED if any of the filters returns REJECTED,
* Status.ALLOWED if any filter returns ALLOWED;
* otherwise returns {@code otherStatus}
*/
public ObjectInputFilter.Status checkInput(FilterInfo info) {
if (info.serialClass() == null)
return UNDECIDED;
Status status = otherStatus;
for (ObjectInputFilter filter : filters) {
Status aStatus = filter.checkInput(info);
if (REJECTED.equals(aStatus)) {
return REJECTED;
}
if (ALLOWED.equals(aStatus)) {
status = ALLOWED;
}
}
return status;
}
@Override
public String toString() {
return "mergeManyFilter(" + filters + ")";
}
}
/**
* An ObjectInputFilter that allows a class only if the class was loaded by the platform class loader.
* Otherwise, it returns undecided; leaving the choice to another filter.
*/
private static class AllowPlatformClassFilter implements ObjectInputFilter {
/**
* Returns ALLOWED only if the class, if non-null, was loaded by the platformClassLoader.
*
* @param filter the FilterInfo
* @return Status.ALLOWED only if the class loader of the class was the PlatformClassLoader;
* otherwise Status.UNDECIDED
*/
public ObjectInputFilter.Status checkInput(FilterInfo filter) {
final Class<?> serialClass = filter.serialClass();
return (serialClass != null &&
(serialClass.getClassLoader() == null ||
ClassLoader.getPlatformClassLoader().equals(serialClass.getClassLoader())))
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.UNDECIDED;
}
public String toString() {
return "allowPlatformClasses";
}
}
}
/**
* FilterInfo instance with a specific class.
*/
static class SerialInfo implements ObjectInputFilter.FilterInfo {
private final Class<?> clazz;
SerialInfo(Class<?> clazz) {
this.clazz = clazz;
}
@Override
public Class<?> serialClass() {
return clazz;
}
@Override
public long arrayLength() {
return 0;
}
@Override
public long depth() {
return 0;
}
@Override
public long references() {
return 0;
}
@Override
public long streamBytes() {
return 0;
}
}
/**
* A test class.
*/
static record Point(int x, int y) implements Serializable {
}
}

View File

@ -0,0 +1,411 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputFilter;
import java.io.ObjectInputFilter.Config;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.io.SerializablePermission;
import java.security.AccessControlException;
import java.security.Permission;
import java.util.function.BinaryOperator;
/* @test
* @build SerialFilterFactoryTest
* @run testng/othervm SerialFilterFactoryTest
* @run testng/othervm -Djdk.serialFilter="*" -Djdk.serialFilterFactory=OVERRIDE SerialFilterFactoryTest
* @run testng/othervm -Djdk.serialFilterFactory=SerialFilterFactoryTest$PropertyFilterFactory SerialFilterFactoryTest
* @run testng/othervm -Djdk.serialFilterFactory=SerialFilterFactoryTest$NotMyFilterFactory SerialFilterFactoryTest
* @run testng/othervm/policy=security.policy
* -Djava.security.properties=${test.src}/java.security-extra-factory
* -Djava.security.debug=properties SerialFilterFactoryTest
* @run testng/othervm/fail -Djdk.serialFilterFactory=ForcedError_NoSuchClass SerialFilterFactoryTest
* @run testng/othervm/policy=security.policy SerialFilterFactoryTest
* @run testng/othervm/policy=security.policy.without.globalFilter SerialFilterFactoryTest
*
* @summary Test Context-specific Deserialization Filters
*/
@Test
public class SerialFilterFactoryTest {
// A stream with just the header, enough to create a OIS
private static final byte[] simpleStream = simpleStream();
private static final Validator v1 = new Validator("v1");
private static final Validator v2 = new Validator("v2");
private static final BinaryOperator<ObjectInputFilter> jdkSerialFilterFactory
= Config.getSerialFilterFactory();
private static final MyFilterFactory contextFilterFactory = new MyFilterFactory("DynFF");
private static final String jdkSerialFilterFactoryProp = System.getProperty("jdk.serialFilterFactory");
/**
* Return a byte array with a simple stream containing an Dummy object.
* @return a byte with a simple serialization object
*/
private static byte[] simpleStream() {
ByteArrayOutputStream boas = new ByteArrayOutputStream();
try (ObjectOutputStream ois = new ObjectOutputStream(boas)) {
ois.writeObject(new Dummy("Here"));
return boas.toByteArray();
} catch (IOException ioe) {
Assert.fail("unexpected IOE", ioe);
}
throw new RuntimeException("should not reach here");
}
/**
* Initialize the filter factory, supplying one if not already set.
* Does not/can not replace any MyFilterFactory.
*
* @param dynFilterFactory a filter factory to use if not already set
* @return the filter factory in effect
*/
private static MyFilterFactory setupFilterFactory(MyFilterFactory dynFilterFactory) {
if ((Config.getSerialFilterFactory() instanceof MyFilterFactory ff))
return ff;
Config.setSerialFilterFactory(dynFilterFactory);
return dynFilterFactory;
}
// If the configured filter has not been set, set it
// It can only be set once for the process, so avoid setting it again
private static ObjectInputFilter setupFilter(ObjectInputFilter serialFilter) {
var configFilter = Config.getSerialFilter();
if (configFilter == serialFilter || configFilter instanceof Validator)
return configFilter; // if already set or a type we can use, no change
if (configFilter == null) {
Config.setSerialFilter(serialFilter);
return serialFilter; // none set already, set it
}
return configFilter;
}
private static boolean isValidFilterFactory() {
return !(ObjectInputFilter.Config.getSerialFilterFactory() instanceof NotMyFilterFactory);
}
/**
* Returns true if serialFilter actions are ok, either no SM or SM has serialFilter Permission
*/
private static boolean hasFilterPerm() {
boolean hasSerialPerm = true;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
try {
Permission p = new SerializablePermission("serialFilter");
sm.checkPermission(p);
hasSerialPerm = true;
} catch (AccessControlException ace2) {
hasSerialPerm = false; // SM and serialFilter not allowed
}
}
return hasSerialPerm;
}
@DataProvider(name="FilterCases")
static Object[][] filterCases() {
if (isValidFilterFactory()) {
return new Object[][]{
{contextFilterFactory, null, null}, // no overrides
{contextFilterFactory, v1, null}, // context filter
{contextFilterFactory, v1, v2}, // per stream filter
};
} else {
// There are zero cases to run with an unknown filter factory. (NotMyFilterFactory)
return new Object[0][0];
}
}
// Setting the filter factory to null is not allowed.
@Test(expectedExceptions=NullPointerException.class)
void testNull() {
Config.setSerialFilterFactory(null);
}
/**
* Setting and resetting the filter factory is not allowed.
* The filter factory may have been on the command line (depending on which @run this is).
* If the jdk.SerialFilterFactory is the built-in filter factory, set it once.
* Try to set it again, the second should throw.
*/
@Test
void testSecondSetShouldThrow() {
if (System.getSecurityManager() != null) {
// Skip test when running with SM
return;
}
var currFF = Config.getSerialFilterFactory();
if (currFF.getClass().getClassLoader() == null) {
try {
// Not already set, set it
Config.setSerialFilterFactory(contextFilterFactory);
currFF = contextFilterFactory;
} catch (IllegalStateException ise) {
Assert.fail("First setSerialFilterFactory should not throw");
}
}
// Setting it again will throw
Assert.expectThrows(IllegalStateException.class,
() -> Config.setSerialFilterFactory(new MyFilterFactory("f11")));
var resetFF = Config.getSerialFilterFactory();
Assert.assertEquals(resetFF, currFF, "Setting again should not change filter factory");
}
/**
* Test that the filter factory is set when expected and is called when expected.
* This test only covers the cases when a filter factory is supplied
* either via a command line property or via the API.
* The cases where the builtin filter factory applies are tested in SerialFilterTest.
*
* @param dynFilterFactory a FilterFactory to set
* @param dynFilter a serial filter to be used for the configured filter
* @param streamFilter a serial filter to be used for the stream filter
* @throws IOException if an I/O error occurs (should not occur)
* @throws ClassNotFoundException for class not found (should not occur)
*/
@Test(dataProvider="FilterCases")
void testCase(MyFilterFactory dynFilterFactory, Validator dynFilter, Validator streamFilter)
throws IOException, ClassNotFoundException {
// Set the Filter Factory and System-wide filter
ObjectInputFilter configFilter;
MyFilterFactory factory;
try {
configFilter = setupFilter(dynFilter);
factory = setupFilterFactory(dynFilterFactory);
Assert.assertTrue(hasFilterPerm(),
"setSerialFilterFactory and setFilterFactory succeeded without serialFilter permission");
} catch (AccessControlException ace) {
Assert.assertFalse(hasFilterPerm(),
"setSerialFilterFactory failed even with serialFilter permission");
return; // test complete
}
factory.reset();
InputStream is = new ByteArrayInputStream(simpleStream);
ObjectInputStream ois = new ObjectInputStream(is);
Assert.assertNull(factory.current(), "initially current should be null");
Assert.assertEquals(factory.next(), configFilter, "initially next should be the configured filter");
var currFilter = ois.getObjectInputFilter();
if (currFilter != null && currFilter.getClass().getClassLoader() == null) {
// Builtin loader; defaults to configured filter
Assert.assertEquals(currFilter, configFilter, "getObjectInputFilter should be configured filter");
} else {
Assert.assertEquals(currFilter, configFilter, "getObjectInputFilter should be null");
}
if (streamFilter != null) {
ois.setObjectInputFilter(streamFilter);
// MyFilterFactory is called when the stream filter is changed; verify values passed it
Assert.assertEquals(factory.current(), currFilter, "when setObjectInputFilter, current should be current filter");
Assert.assertEquals(factory.next(), streamFilter, "next should be stream specific filter");
// Check the OIS filter after the factory has updated it.
currFilter = ois.getObjectInputFilter();
Assert.assertEquals(currFilter, streamFilter, "getObjectInputFilter should be set");
// Verify that it can not be set again
Assert.assertThrows(IllegalStateException.class, () -> ois.setObjectInputFilter(streamFilter));
}
if (currFilter instanceof Validator validator) {
validator.reset();
Object o = ois.readObject(); // Invoke only for the side effect of calling the Filter
Assert.assertEquals(validator.count, 1, "Wrong number of calls to the stream filter");
} else {
Object o = ois.readObject(); // Invoke only for the side effect of calling the filter
}
}
// Test that if the property jdk-serialFilterFactory is set, then initial factory has the same classname
@Test
void testPropertyFilterFactory() {
if (jdkSerialFilterFactoryProp != null && !jdkSerialFilterFactoryProp.equals("OVERRIDE")) {
Assert.assertEquals(jdkSerialFilterFactory.getClass().getName(), jdkSerialFilterFactoryProp,
"jdk.serialFilterFactory property classname mismatch");
}
}
// Test that setting the filter factory after any deserialization (any testCase)
// throws IllegalStateException with the specific message
@Test(dependsOnMethods="testCase")
void testSetFactoryAfterDeserialization() {
if (hasFilterPerm()) {
// Only test if is allowed by SM.
BinaryOperator<ObjectInputFilter> factory = Config.getSerialFilterFactory();
IllegalStateException ise = Assert.expectThrows(IllegalStateException.class, () -> Config.setSerialFilterFactory(factory));
Assert.assertTrue(ise.getMessage().startsWith("Cannot replace filter factory: "));
}
}
// Test that OIS.setObjectInputFilter does not allow a null filter to replace
// a non-null filter. And does allow a null filter to replace a null filter
@Test
void testDisableFailFilter() throws IOException {
if (hasFilterPerm()) {
// Only test if is allowed by SM.
ObjectInputFilter curr = null;
try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(simpleStream))) {
curr = ois.getObjectInputFilter();
// Try to set the filter to null
ois.setObjectInputFilter(null);
if (curr != null) {
Assert.fail("setting filter to null after a non-null filter should throw");
}
} catch (IllegalStateException ise) {
if (curr == null) {
Assert.fail("setting filter to null after a null filter should not throw");
}
}
}
}
/**
* A simple filter factory that retains its arguments.
*/
private static class MyFilterFactory
implements BinaryOperator<ObjectInputFilter> {
private final String name;
private ObjectInputFilter current;
private ObjectInputFilter next;
MyFilterFactory(String name) {
this.name = name;
current = new Validator("UnsetCurrent");
next = new Validator("UnsetNext");
}
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
this.current = curr;
this.next = next;
if (curr == null & next == null)
return Config.getSerialFilter(); // Default to the configured filter
return next;
}
public void reset() {
current = new Validator("UnsetCurrent");
next = new Validator("UnsetNext");
}
public ObjectInputFilter current() {
return current;
}
public ObjectInputFilter next() {
return next;
}
public String toString() {
return name + ":: curr: " + current + ", next: " + next;
}
}
/**
* A subclass of MyFilterFactory with a name, used when testing setting the factory using
* -Djdk.setFilterFactory.
*/
public static class PropertyFilterFactory extends MyFilterFactory {
public PropertyFilterFactory() {
super("UNNAMED");
}
}
/**
* A filter factory that is not compatible with MyFilterFactory test.
* Used for testing incorrect initialization.
*/
public static class NotMyFilterFactory
implements BinaryOperator<ObjectInputFilter> {
public NotMyFilterFactory() {}
/**
* Returns null as the filter to be used for an ObjectInputStream.
*
* @param curr the current filter, if any
* @param next the next filter, if any
* @return null
*/
public ObjectInputFilter apply(ObjectInputFilter curr, ObjectInputFilter next) {
return null;
}
}
/**
* A filter that accumulates information about the checkInput callbacks
* that can be checked after readObject completes.
*/
static class Validator implements ObjectInputFilter {
private final String name;
long count; // Count of calls to checkInput
Validator(String name) {
this.name = name;
count = 0;
}
void reset() {
count = 0;
}
@Override
public Status checkInput(FilterInfo filter) {
count++;
return Status.ALLOWED;
}
public String toString(){
return name + ": count: " + count;
}
}
/**
* A simple class to serialize.
*/
private static final class Dummy implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
final String s;
Dummy(String s) {
this.s = s;
}
public String toString() {
return this.getClass().getName() + "::" + s;
}
}
}

View File

@ -0,0 +1,228 @@
/*
* Copyright (c) 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import org.testng.Assert;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import java.io.ObjectInputFilter;
import java.io.ObjectInputFilter.FilterInfo;
import java.util.function.Predicate;
import static java.io.ObjectInputFilter.Status;
import static java.io.ObjectInputFilter.Status.ALLOWED;
import static java.io.ObjectInputFilter.Status.REJECTED;
import static java.io.ObjectInputFilter.Status.UNDECIDED;
/* @test
* @run testng/othervm -Djdk.serialFilterTrace=true SerialFilterFunctionTest
* @summary ObjectInputFilter.Config Function Tests
*/
@Test
public class SerialFilterFunctionTest {
@Test
void testMerge() {
Status[] cases = Status.values();
FilterInfo info = new SerialInfo(Object.class);
for (Status st1 : cases) {
ObjectInputFilter filter1 = getFilter(st1);
for (Status st2 : cases) {
ObjectInputFilter filter2 = getFilter(st2);
ObjectInputFilter f = ObjectInputFilter.merge(filter1, filter2);
Status r = f.checkInput(info);
Assert.assertEquals(merge(st1, st2), r, "merge");
}
Assert.assertSame(ObjectInputFilter.merge(filter1, null), filter1, "merge with null fail");
Assert.assertThrows(NullPointerException.class, () -> ObjectInputFilter.merge(null, filter1));
}
}
/**
* Return REJECTED if either is REJECTED; otherwise return ALLOWED if either is ALLOWED, else UNDECIDED.
* @param status a status
* @param otherStatus another status
* @return REJECTED if either is REJECTED; otherwise return ALLOWED if either is ALLOWED, else UNDECIDED
*/
private Status merge(Status status, Status otherStatus) {
if (REJECTED.equals(status) || REJECTED.equals(otherStatus))
return REJECTED;
if (ALLOWED.equals(status) || ALLOWED.equals(otherStatus))
return ALLOWED;
return UNDECIDED;
}
/**
* Return a predicate mapping Class<?> to a boolean that returns true if the argument is Integer.class.
* @return a predicate mapping Class<?> to a boolean that returns true if the argument is Integer.class
*/
static Predicate<Class<?>> isInteger() {
return (cl) -> cl.equals(Integer.class);
}
@DataProvider(name = "AllowPredicateCases")
static Object[][] allowPredicateCases() {
return new Object[][]{
{ Integer.class, isInteger(), REJECTED, ALLOWED},
{ Double.class, isInteger(), REJECTED, REJECTED},
{ null, isInteger(), REJECTED, UNDECIDED}, // no class -> UNDECIDED
{ Double.class, isInteger(), null, null}, // NPE
{ Double.class, null, REJECTED, null}, // NPE
};
}
@Test(dataProvider = "AllowPredicateCases")
void testAllowPredicates(Class<?> clazz, Predicate<Class<?>> predicate, Status otherStatus, Status expected) {
ObjectInputFilter.FilterInfo info = new SerialInfo(clazz);
if (predicate == null || expected == null) {
Assert.assertThrows(NullPointerException.class, () -> ObjectInputFilter.allowFilter(predicate, expected));
} else {
Assert.assertEquals(ObjectInputFilter.allowFilter(predicate, otherStatus).checkInput(info),
expected, "Predicate result");
}
}
@DataProvider(name = "RejectPredicateCases")
static Object[][] rejectPredicateCases() {
return new Object[][]{
{ Integer.class, isInteger(), REJECTED, REJECTED},
{ Double.class, isInteger(), ALLOWED, ALLOWED},
{ null, isInteger(), REJECTED, UNDECIDED}, // no class -> UNDECIDED
{ Double.class, isInteger(), null, null}, // NPE
{ Double.class, null, UNDECIDED, null}, // NPE
};
}
@Test(dataProvider = "RejectPredicateCases")
void testRejectPredicates(Class<?> clazz, Predicate<Class<?>> predicate, Status otherStatus, Status expected) {
ObjectInputFilter.FilterInfo info = new SerialInfo(clazz);
if (predicate == null || expected == null) {
Assert.assertThrows(NullPointerException.class, () -> ObjectInputFilter.allowFilter(predicate, expected));
} else {
Assert.assertEquals(ObjectInputFilter.rejectFilter(predicate, otherStatus)
.checkInput(info), expected, "Predicate result");
}
}
@Test
void testRejectUndecided() {
FilterInfo info = new SerialInfo(Object.class); // an info structure, unused
ObjectInputFilter undecided = getFilter(UNDECIDED);
Assert.assertEquals(ObjectInputFilter.rejectUndecidedClass(undecided).checkInput(info), REJECTED, "undecided -> rejected");
ObjectInputFilter allowed = getFilter(ALLOWED);
Assert.assertEquals(ObjectInputFilter.rejectUndecidedClass(allowed).checkInput(info), ALLOWED, "allowed -> rejected");
ObjectInputFilter rejected = getFilter(REJECTED);
Assert.assertEquals(ObjectInputFilter.rejectUndecidedClass(rejected).checkInput(info), REJECTED, "rejected -> rejected");
// Specific cases of Classes the result in allowed, rejected, and undecided status
ObjectInputFilter numberFilter = ObjectInputFilter.Config.createFilter("java.lang.Integer;!java.lang.Double");
Object[] testObjs = {
Integer.valueOf(1), // Integer is allowed -> allowed
new Integer[1], // Integer is allowed -> allowed
new Integer[0][0][0], // Integer is allowed -> allowed
Long.valueOf(2), // Long is undecided -> rejected
new Long[1], // Long is undecided -> rejected
new Long[0][0][0], // Long is undecided -> rejected
Double.valueOf(2.0d), // Double is rejected -> rejected
new Double[1], // Double is rejected -> rejected
new Double[0][0][0], // Double is rejected -> rejected
new int[1], // int is primitive undecided -> undecided
new int[1][1][1], // int is primitive undecided -> undecided
};
for (Object obj : testObjs) {
Class<?> clazz = obj.getClass();
info = new SerialInfo(clazz);
Status rawSt = numberFilter.checkInput(info);
Status st = ObjectInputFilter.rejectUndecidedClass(numberFilter).checkInput(info);
if (UNDECIDED.equals(rawSt)) {
while (clazz.isArray())
clazz = clazz.getComponentType();
Status expected = (clazz.isPrimitive()) ? UNDECIDED : REJECTED;
Assert.assertEquals(st, expected, "Wrong status for class: " + obj.getClass());
} else {
Assert.assertEquals(rawSt, st, "raw filter and rejectUndecided filter disagree");
}
}
}
/**
* Returns an ObjectInputFilter that returns the requested Status.
* @param status a Status, may be null
* @return an ObjectInputFilter that returns the requested Status
*/
private static ObjectInputFilter getFilter(ObjectInputFilter.Status status) {
return (info) -> status;
}
/**
* FilterInfo instance with a specific class.
*/
static class SerialInfo implements ObjectInputFilter.FilterInfo {
private final Class<?> clazz;
SerialInfo(Class<?> clazz) {
this.clazz = clazz;
}
@Override
public Class<?> serialClass() {
return clazz;
}
@Override
public long arrayLength() {
return 0;
}
@Override
public long depth() {
return 0;
}
@Override
public long references() {
return 0;
}
@Override
public long streamBytes() {
return 0;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("serialClass: " + serialClass());
sb.append(", arrayLength: " + arrayLength());
sb.append(", depth: " + depth());
sb.append(", references: " + references());
sb.append(", streamBytes: " + streamBytes());
return sb.toString();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2020, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2021, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -29,6 +29,7 @@ import java.io.InvalidClassException;
import java.io.ObjectInputFilter;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serial;
import java.io.Serializable;
import java.lang.invoke.SerializedLambda;
import java.lang.reflect.Constructor;
@ -52,14 +53,15 @@ import org.testng.annotations.DataProvider;
/* @test
* @bug 8234836
* @build SerialFilterTest
* @run testng/othervm SerialFilterTest
* @run testng/othervm -Djdk.serialSetFilterAfterRead=true SerialFilterTest
* @run testng/othervm -Djdk.serialFilterTrace=true SerialFilterTest
* @run testng/othervm -Djdk.serialSetFilterAfterRead=true -Djdk.serialFilterTrace=true SerialFilterTest
*
* @summary Test ObjectInputFilters
* @summary Test ObjectInputFilters using Builtin Filter Factory
*/
@Test
public class SerialFilterTest implements Serializable {
@Serial
private static final long serialVersionUID = -6999613679881262446L;
/**
@ -242,7 +244,7 @@ public class SerialFilterTest implements Serializable {
* @throws IOException
*/
@Test(dataProvider="Objects")
public static void t1(Object object,
void t1(Object object,
long count, long maxArray, long maxRefs, long maxDepth, long maxBytes,
List<Class<?>> classes) throws IOException {
byte[] bytes = writeObjects(object);
@ -267,7 +269,7 @@ public class SerialFilterTest implements Serializable {
* @param pattern a pattern
*/
@Test(dataProvider="Patterns")
static void testPatterns(String pattern) {
void testPatterns(String pattern) {
evalPattern(pattern, (p, o, neg) -> testPatterns(p, o, neg));
}
@ -277,40 +279,41 @@ public class SerialFilterTest implements Serializable {
* This test is agnostic the global filter being set or not.
*/
@Test
static void nonResettableFilter() {
void nonResettableFilter() {
Validator validator1 = new Validator();
Validator validator2 = new Validator();
try {
byte[] bytes = writeObjects("text1"); // an object
Validator[] filterCases = {
validator1, // setting filter to a non-null filter
null, // setting stream-specific filter to null
};
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais)) {
// Check the initial filter is the global filter; may be null
ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter();
ObjectInputFilter initial = ois.getObjectInputFilter();
Assert.assertEquals(global, initial, "initial filter should be the global filter");
for (Validator validator : filterCases) {
try {
byte[] bytes = writeObjects("text1"); // an object
// Check if it can be set to null
ois.setObjectInputFilter(null);
ObjectInputFilter filter = ois.getObjectInputFilter();
Assert.assertNull(filter, "set to null should be null");
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais)) {
// Check the initial filter is the global filter; may be null
ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter();
ObjectInputFilter initial = ois.getObjectInputFilter();
Assert.assertEquals(global, initial, "initial filter should be the global filter");
ois.setObjectInputFilter(validator1);
Object o = ois.readObject();
try {
ois.setObjectInputFilter(validator2);
Assert.fail("Should not be able to set filter twice");
} catch (IllegalStateException ise) {
// success, the exception was expected
ois.setObjectInputFilter(validator);
Object o = ois.readObject();
try {
ois.setObjectInputFilter(validator2);
Assert.fail("Should not be able to set filter twice");
} catch (IllegalStateException ise) {
// success, the exception was expected
}
} catch (EOFException eof) {
Assert.fail("Should not reach end-of-file", eof);
} catch (ClassNotFoundException cnf) {
Assert.fail("Deserializing", cnf);
}
} catch (EOFException eof) {
Assert.fail("Should not reach end-of-file", eof);
} catch (ClassNotFoundException cnf) {
Assert.fail("Deserializing", cnf);
} catch (IOException ex) {
Assert.fail("Unexpected IOException", ex);
}
} catch (IOException ex) {
Assert.fail("Unexpected IOException", ex);
}
}
@ -323,7 +326,7 @@ public class SerialFilterTest implements Serializable {
* to revert to the old behavior but it re-enables the incorrect use.
*/
@Test
static void testNonSettableAfterReadObject() throws IOException, ClassNotFoundException {
void testNonSettableAfterReadObject() throws IOException, ClassNotFoundException {
String expected1 = "text1";
String expected2 = "text2";
byte[] bytes = writeObjects(expected1, expected2);
@ -359,7 +362,7 @@ public class SerialFilterTest implements Serializable {
* @throws IOException if an error occurs
*/
@Test(dataProvider="Arrays")
static void testReadResolveToArray(Object array, int length) throws IOException {
void testReadResolveToArray(Object array, int length) throws IOException {
ReadResolveToArray object = new ReadResolveToArray(array, length);
byte[] bytes = writeObjects(object);
Object o = validate(bytes, object); // the object is its own filter
@ -376,7 +379,7 @@ public class SerialFilterTest implements Serializable {
* @param value a test value
*/
@Test(dataProvider="Limits")
static void testLimits(String name, long value) {
void testLimits(String name, long value) {
Class<?> arrayClass = new int[0].getClass();
String pattern = String.format("%s=%d;%s=%d", name, value, name, value - 1);
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
@ -396,7 +399,7 @@ public class SerialFilterTest implements Serializable {
* @param pattern a pattern to test
*/
@Test(dataProvider="InvalidLimits", expectedExceptions=java.lang.IllegalArgumentException.class)
static void testInvalidLimits(String pattern) {
void testInvalidLimits(String pattern) {
try {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
} catch (IllegalArgumentException iae) {
@ -409,7 +412,7 @@ public class SerialFilterTest implements Serializable {
* Test that returning null from a filter causes deserialization to fail.
*/
@Test(expectedExceptions=InvalidClassException.class)
static void testNullStatus() throws IOException {
void testNullStatus() throws IOException {
byte[] bytes = writeObjects(0); // an Integer
try {
Object o = validate(bytes, new ObjectInputFilter() {
@ -428,7 +431,7 @@ public class SerialFilterTest implements Serializable {
* @param pattern pattern from the data source
*/
@Test(dataProvider="InvalidPatterns", expectedExceptions=IllegalArgumentException.class)
static void testInvalidPatterns(String pattern) {
void testInvalidPatterns(String pattern) {
try {
ObjectInputFilter.Config.createFilter(pattern);
} catch (IllegalArgumentException iae) {
@ -441,7 +444,7 @@ public class SerialFilterTest implements Serializable {
* Test that Config.create returns null if the argument does not contain any patterns or limits.
*/
@Test()
static void testEmptyPattern() {
void testEmptyPattern() {
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("");
Assert.assertNull(filter, "empty pattern did not return null");
@ -773,6 +776,7 @@ public class SerialFilterTest implements Serializable {
* the ObjectInputFilter to check that it has the expected length.
*/
static class ReadResolveToArray implements Serializable, ObjectInputFilter {
@Serial
private static final long serialVersionUID = 123456789L;
@SuppressWarnings("serial") /* Incorrect declarations are being tested */
@ -784,6 +788,7 @@ public class SerialFilterTest implements Serializable {
this.length = length;
}
@Serial
Object readResolve() {
return array;
}
@ -844,21 +849,27 @@ public class SerialFilterTest implements Serializable {
// Deeper superclass hierarchy
static class A implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
};
static class B extends A {
@Serial
private static final long serialVersionUID = 2L;
}
static class C extends B {
@Serial
private static final long serialVersionUID = 3L;
}
static class D extends C {
@Serial
private static final long serialVersionUID = 4L;
}
static class E extends D {
@Serial
private static final long serialVersionUID = 5L;
}
static class F extends E {
@Serial
private static final long serialVersionUID = 6L;
}

View File

@ -0,0 +1,4 @@
# Deserialization Input Filter Factory
# See conf/security/java.security for pattern synatx
#
jdk.serialFilterFactory=SerialFilterFactoryTest$PropertyFilterFactory