mirror of
https://github.com/openjdk/jdk.git
synced 2025-08-27 06:45:07 +02:00
8264859: Implement Context-Specific Deserialization Filters
Reviewed-by: bchristi, dfuchs, chegar
This commit is contained in:
parent
dd34a4c28d
commit
13d6180421
9 changed files with 2556 additions and 187 deletions
File diff suppressed because it is too large
Load diff
|
@ -25,6 +25,7 @@
|
||||||
|
|
||||||
package java.io;
|
package java.io;
|
||||||
|
|
||||||
|
import java.io.ObjectInputFilter.Config;
|
||||||
import java.io.ObjectStreamClass.WeakClassKey;
|
import java.io.ObjectStreamClass.WeakClassKey;
|
||||||
import java.io.ObjectStreamClass.RecordSupport;
|
import java.io.ObjectStreamClass.RecordSupport;
|
||||||
import java.lang.System.Logger;
|
import java.lang.System.Logger;
|
||||||
|
@ -66,6 +67,23 @@ import sun.security.action.GetIntegerAction;
|
||||||
* practices for defensive use of serial filters.
|
* practices for defensive use of serial filters.
|
||||||
* </strong></p>
|
* </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
|
* <p>ObjectOutputStream and ObjectInputStream can provide an application with
|
||||||
* persistent storage for graphs of objects when used with a FileOutputStream
|
* persistent storage for graphs of objects when used with a FileOutputStream
|
||||||
* and FileInputStream respectively. ObjectInputStream is used to recover
|
* 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
|
* protected) or that there are get and set methods that can be used to restore
|
||||||
* the state.
|
* 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
|
* <p>Any exception that occurs while deserializing an object will be caught by
|
||||||
* the ObjectInputStream and abort the reading process.
|
* the ObjectInputStream and abort the reading process.
|
||||||
*
|
*
|
||||||
|
@ -347,14 +355,20 @@ public class ObjectInputStream
|
||||||
*/
|
*/
|
||||||
private ObjectInputFilter serialFilter;
|
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.
|
* Creates an ObjectInputStream that reads from the specified InputStream.
|
||||||
* A serialization stream header is read from the stream and verified.
|
* A serialization stream header is read from the stream and verified.
|
||||||
* This constructor will block until the corresponding ObjectOutputStream
|
* This constructor will block until the corresponding ObjectOutputStream
|
||||||
* has written and flushed the header.
|
* has written and flushed the header.
|
||||||
*
|
*
|
||||||
* <p>The serialization filter is initialized to the value of
|
* <p>The constructor initializes the deserialization filter to the filter returned
|
||||||
* {@linkplain ObjectInputFilter.Config#getSerialFilter() the system-wide filter}.
|
* 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
|
* <p>If a security manager is installed, this constructor will check for
|
||||||
* the "enableSubclassImplementation" SerializablePermission when invoked
|
* the "enableSubclassImplementation" SerializablePermission when invoked
|
||||||
|
@ -377,7 +391,8 @@ public class ObjectInputStream
|
||||||
bin = new BlockDataInputStream(in);
|
bin = new BlockDataInputStream(in);
|
||||||
handles = new HandleTable(10);
|
handles = new HandleTable(10);
|
||||||
vlist = new ValidationList();
|
vlist = new ValidationList();
|
||||||
serialFilter = ObjectInputFilter.Config.getSerialFilter();
|
streamFilterSet = false;
|
||||||
|
serialFilter = Config.getSerialFilterFactorySingleton().apply(null, Config.getSerialFilter());
|
||||||
enableOverride = false;
|
enableOverride = false;
|
||||||
readStreamHeader();
|
readStreamHeader();
|
||||||
bin.setBlockDataMode(true);
|
bin.setBlockDataMode(true);
|
||||||
|
@ -388,8 +403,9 @@ public class ObjectInputStream
|
||||||
* ObjectInputStream to not have to allocate private data just used by this
|
* ObjectInputStream to not have to allocate private data just used by this
|
||||||
* implementation of ObjectInputStream.
|
* implementation of ObjectInputStream.
|
||||||
*
|
*
|
||||||
* <p>The serialization filter is initialized to the value of
|
* <p>The constructor initializes the deserialization filter to the filter returned
|
||||||
* {@linkplain ObjectInputFilter.Config#getSerialFilter() the system-wide filter}.
|
* 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
|
* <p>If there is a security manager installed, this method first calls the
|
||||||
* security manager's {@code checkPermission} method with the
|
* security manager's {@code checkPermission} method with the
|
||||||
|
@ -412,7 +428,8 @@ public class ObjectInputStream
|
||||||
bin = null;
|
bin = null;
|
||||||
handles = null;
|
handles = null;
|
||||||
vlist = null;
|
vlist = null;
|
||||||
serialFilter = ObjectInputFilter.Config.getSerialFilter();
|
streamFilterSet = false;
|
||||||
|
serialFilter = Config.getSerialFilterFactorySingleton().apply(null, Config.getSerialFilter());
|
||||||
enableOverride = true;
|
enableOverride = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -431,7 +448,7 @@ public class ObjectInputStream
|
||||||
* priorities. The callbacks are registered by objects (in the readObject
|
* priorities. The callbacks are registered by objects (in the readObject
|
||||||
* special methods) as they are individually restored.
|
* 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.
|
* each object (regular or class) read to reconstruct the root object.
|
||||||
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
|
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
|
||||||
*
|
*
|
||||||
|
@ -443,7 +460,7 @@ public class ObjectInputStream
|
||||||
* @throws ClassNotFoundException Class of a serialized object cannot be
|
* @throws ClassNotFoundException Class of a serialized object cannot be
|
||||||
* found.
|
* found.
|
||||||
* @throws InvalidClassException Something is wrong with a class used by
|
* @throws InvalidClassException Something is wrong with a class used by
|
||||||
* serialization.
|
* deserialization.
|
||||||
* @throws StreamCorruptedException Control information in the
|
* @throws StreamCorruptedException Control information in the
|
||||||
* stream is inconsistent.
|
* stream is inconsistent.
|
||||||
* @throws OptionalDataException Primitive data was found in the
|
* @throws OptionalDataException Primitive data was found in the
|
||||||
|
@ -564,7 +581,7 @@ public class ObjectInputStream
|
||||||
* invocation of readObject or readUnshared on the ObjectInputStream,
|
* invocation of readObject or readUnshared on the ObjectInputStream,
|
||||||
* even if the underlying data stream has been manipulated.
|
* 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.
|
* each object (regular or class) read to reconstruct the root object.
|
||||||
* See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
|
* 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
|
* <p>When a subclass is replacing objects it must insure that the
|
||||||
* substituted object is compatible with every field where the reference
|
* 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
|
* 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.
|
* and the object is not be stored.
|
||||||
*
|
*
|
||||||
* <p>This method is called only once when each object is first
|
* <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.
|
* Returns the deserialization filter for this stream.
|
||||||
* The serialization filter is the most recent filter set in
|
* The filter is the result of invoking the
|
||||||
* {@link #setObjectInputFilter setObjectInputFilter} or
|
* {@link Config#getSerialFilterFactory() JVM-wide filter factory}
|
||||||
* the initial system-wide filter from
|
* either by the {@linkplain #ObjectInputStream() constructor} or the most recent invocation of
|
||||||
* {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}.
|
* {@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
|
* @since 9
|
||||||
*/
|
*/
|
||||||
public final ObjectInputFilter getObjectInputFilter() {
|
public final ObjectInputFilter getObjectInputFilter() {
|
||||||
|
@ -1237,8 +1254,23 @@ public class ObjectInputStream
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the serialization filter for the stream.
|
* Set the deserialization filter for the stream.
|
||||||
* The filter's {@link ObjectInputFilter#checkInput checkInput} method is called
|
*
|
||||||
|
* 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.
|
* for each class and reference in the stream.
|
||||||
* The filter can check any or all of the class, the array length, the number
|
* 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.
|
* 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.
|
* and the current object being deserialized.
|
||||||
* The number of references is the cumulative number of objects and references
|
* 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.
|
* 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
|
* The filter is invoked only when reading objects from the stream and not for
|
||||||
* not primitives.
|
* primitives.
|
||||||
* <p>
|
* <p>
|
||||||
* If the filter returns {@link ObjectInputFilter.Status#REJECTED Status.REJECTED},
|
* If the filter returns {@link ObjectInputFilter.Status#REJECTED Status.REJECTED},
|
||||||
* {@code null} or throws a {@link RuntimeException},
|
* {@code null} or throws a {@link RuntimeException},
|
||||||
* the active {@code readObject} or {@code readUnshared}
|
* the active {@code readObject} or {@code readUnshared}
|
||||||
* throws {@link InvalidClassException}, otherwise deserialization
|
* throws {@link InvalidClassException}, otherwise deserialization
|
||||||
* continues uninterrupted.
|
* 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
|
* @implSpec
|
||||||
* The filter, when not {@code null}, is invoked during {@link #readObject readObject}
|
* 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
|
* @param filter the filter, may be null
|
||||||
* @throws SecurityException if there is security manager and the
|
* @throws SecurityException if there is security manager and the
|
||||||
* {@code SerializablePermission("serialFilter")} is not granted
|
* {@code SerializablePermission("serialFilter")} is not granted
|
||||||
* @throws IllegalStateException if the {@linkplain #getObjectInputFilter() current filter}
|
* @throws IllegalStateException if an object has been read,
|
||||||
* is not {@code null} and is not the system-wide filter, or
|
* if the filter factory returns {@code null} when the
|
||||||
* if an object has been read
|
* {@linkplain #getObjectInputFilter() current filter} is non-null, or
|
||||||
|
* if the filter has already been set.
|
||||||
* @since 9
|
* @since 9
|
||||||
*/
|
*/
|
||||||
public final void setObjectInputFilter(ObjectInputFilter filter) {
|
public final void setObjectInputFilter(ObjectInputFilter filter) {
|
||||||
|
@ -1314,20 +1340,25 @@ public class ObjectInputStream
|
||||||
if (sm != null) {
|
if (sm != null) {
|
||||||
sm.checkPermission(ObjectStreamConstants.SERIAL_FILTER_PERMISSION);
|
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) {
|
if (totalObjectRefs > 0 && !Caches.SET_FILTER_AFTER_READ) {
|
||||||
throw new IllegalStateException(
|
throw new IllegalStateException(
|
||||||
"filter can not be set after an object has been read");
|
"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.
|
* If the filter rejects or an exception is thrown, throws InvalidClassException.
|
||||||
*
|
*
|
||||||
|
|
|
@ -47,6 +47,7 @@ public final class StaticProperty {
|
||||||
private static final String JAVA_LIBRARY_PATH;
|
private static final String JAVA_LIBRARY_PATH;
|
||||||
private static final String SUN_BOOT_LIBRARY_PATH;
|
private static final String SUN_BOOT_LIBRARY_PATH;
|
||||||
private static final String JDK_SERIAL_FILTER;
|
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 JAVA_IO_TMPDIR;
|
||||||
private static final String NATIVE_ENCODING;
|
private static final String NATIVE_ENCODING;
|
||||||
|
|
||||||
|
@ -62,6 +63,7 @@ public final class StaticProperty {
|
||||||
JAVA_LIBRARY_PATH = getProperty(props, "java.library.path", "");
|
JAVA_LIBRARY_PATH = getProperty(props, "java.library.path", "");
|
||||||
SUN_BOOT_LIBRARY_PATH = getProperty(props, "sun.boot.library.path", "");
|
SUN_BOOT_LIBRARY_PATH = getProperty(props, "sun.boot.library.path", "");
|
||||||
JDK_SERIAL_FILTER = getProperty(props, "jdk.serialFilter", null);
|
JDK_SERIAL_FILTER = getProperty(props, "jdk.serialFilter", null);
|
||||||
|
JDK_SERIAL_FILTER_FACTORY = getProperty(props, "jdk.serialFilterFactory", null);
|
||||||
NATIVE_ENCODING = getProperty(props, "native.encoding");
|
NATIVE_ENCODING = getProperty(props, "native.encoding");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,6 +186,20 @@ public final class StaticProperty {
|
||||||
return JDK_SERIAL_FILTER;
|
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.
|
* Return the {@code native.encoding} system property.
|
||||||
*
|
*
|
||||||
|
|
|
@ -979,11 +979,29 @@ jdk.xml.dsig.secureValidationPolicy=\
|
||||||
noDuplicateIds,\
|
noDuplicateIds,\
|
||||||
noRetrievalMethodLoops
|
noRetrievalMethodLoops
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Serialization system-wide filter
|
# Deserialization system-wide filter factory
|
||||||
#
|
#
|
||||||
# A filter, if configured, is used by java.io.ObjectInputStream during
|
# A filter factory class name is used to configure the system-wide filter factory.
|
||||||
# deserialization to check the contents of the stream.
|
# 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
|
# 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.
|
# matched against the name of a class in the stream or defines a limit.
|
||||||
# Patterns are separated by ";" (semicolon).
|
# Patterns are separated by ";" (semicolon).
|
||||||
|
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.
|
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
|
||||||
*
|
*
|
||||||
* This code is free software; you can redistribute it and/or modify it
|
* 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.ObjectInputFilter;
|
||||||
import java.io.ObjectInputStream;
|
import java.io.ObjectInputStream;
|
||||||
import java.io.ObjectOutputStream;
|
import java.io.ObjectOutputStream;
|
||||||
|
import java.io.Serial;
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.lang.invoke.SerializedLambda;
|
import java.lang.invoke.SerializedLambda;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
|
@ -52,14 +53,15 @@ import org.testng.annotations.DataProvider;
|
||||||
/* @test
|
/* @test
|
||||||
* @bug 8234836
|
* @bug 8234836
|
||||||
* @build SerialFilterTest
|
* @build SerialFilterTest
|
||||||
* @run testng/othervm SerialFilterTest
|
* @run testng/othervm -Djdk.serialFilterTrace=true SerialFilterTest
|
||||||
* @run testng/othervm -Djdk.serialSetFilterAfterRead=true SerialFilterTest
|
* @run testng/othervm -Djdk.serialSetFilterAfterRead=true -Djdk.serialFilterTrace=true SerialFilterTest
|
||||||
*
|
*
|
||||||
* @summary Test ObjectInputFilters
|
* @summary Test ObjectInputFilters using Builtin Filter Factory
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
public class SerialFilterTest implements Serializable {
|
public class SerialFilterTest implements Serializable {
|
||||||
|
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = -6999613679881262446L;
|
private static final long serialVersionUID = -6999613679881262446L;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -242,7 +244,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* @throws IOException
|
* @throws IOException
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="Objects")
|
@Test(dataProvider="Objects")
|
||||||
public static void t1(Object object,
|
void t1(Object object,
|
||||||
long count, long maxArray, long maxRefs, long maxDepth, long maxBytes,
|
long count, long maxArray, long maxRefs, long maxDepth, long maxBytes,
|
||||||
List<Class<?>> classes) throws IOException {
|
List<Class<?>> classes) throws IOException {
|
||||||
byte[] bytes = writeObjects(object);
|
byte[] bytes = writeObjects(object);
|
||||||
|
@ -267,7 +269,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* @param pattern a pattern
|
* @param pattern a pattern
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="Patterns")
|
@Test(dataProvider="Patterns")
|
||||||
static void testPatterns(String pattern) {
|
void testPatterns(String pattern) {
|
||||||
evalPattern(pattern, (p, o, neg) -> testPatterns(p, o, neg));
|
evalPattern(pattern, (p, o, neg) -> testPatterns(p, o, neg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -277,26 +279,26 @@ public class SerialFilterTest implements Serializable {
|
||||||
* This test is agnostic the global filter being set or not.
|
* This test is agnostic the global filter being set or not.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
static void nonResettableFilter() {
|
void nonResettableFilter() {
|
||||||
Validator validator1 = new Validator();
|
Validator validator1 = new Validator();
|
||||||
Validator validator2 = new Validator();
|
Validator validator2 = new Validator();
|
||||||
|
|
||||||
|
Validator[] filterCases = {
|
||||||
|
validator1, // setting filter to a non-null filter
|
||||||
|
null, // setting stream-specific filter to null
|
||||||
|
};
|
||||||
|
|
||||||
|
for (Validator validator : filterCases) {
|
||||||
try {
|
try {
|
||||||
byte[] bytes = writeObjects("text1"); // an object
|
byte[] bytes = writeObjects("text1"); // an object
|
||||||
|
|
||||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
|
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); ObjectInputStream ois = new ObjectInputStream(bais)) {
|
||||||
ObjectInputStream ois = new ObjectInputStream(bais)) {
|
|
||||||
// Check the initial filter is the global filter; may be null
|
// Check the initial filter is the global filter; may be null
|
||||||
ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter();
|
ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter();
|
||||||
ObjectInputFilter initial = ois.getObjectInputFilter();
|
ObjectInputFilter initial = ois.getObjectInputFilter();
|
||||||
Assert.assertEquals(global, initial, "initial filter should be the global filter");
|
Assert.assertEquals(global, initial, "initial filter should be the global filter");
|
||||||
|
|
||||||
// Check if it can be set to null
|
ois.setObjectInputFilter(validator);
|
||||||
ois.setObjectInputFilter(null);
|
|
||||||
ObjectInputFilter filter = ois.getObjectInputFilter();
|
|
||||||
Assert.assertNull(filter, "set to null should be null");
|
|
||||||
|
|
||||||
ois.setObjectInputFilter(validator1);
|
|
||||||
Object o = ois.readObject();
|
Object o = ois.readObject();
|
||||||
try {
|
try {
|
||||||
ois.setObjectInputFilter(validator2);
|
ois.setObjectInputFilter(validator2);
|
||||||
|
@ -313,6 +315,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
Assert.fail("Unexpected IOException", ex);
|
Assert.fail("Unexpected IOException", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* After reading some objects from the stream, setting a filter is disallowed.
|
* After reading some objects from the stream, setting a filter is disallowed.
|
||||||
|
@ -323,7 +326,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* to revert to the old behavior but it re-enables the incorrect use.
|
* to revert to the old behavior but it re-enables the incorrect use.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
static void testNonSettableAfterReadObject() throws IOException, ClassNotFoundException {
|
void testNonSettableAfterReadObject() throws IOException, ClassNotFoundException {
|
||||||
String expected1 = "text1";
|
String expected1 = "text1";
|
||||||
String expected2 = "text2";
|
String expected2 = "text2";
|
||||||
byte[] bytes = writeObjects(expected1, expected2);
|
byte[] bytes = writeObjects(expected1, expected2);
|
||||||
|
@ -359,7 +362,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* @throws IOException if an error occurs
|
* @throws IOException if an error occurs
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="Arrays")
|
@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);
|
ReadResolveToArray object = new ReadResolveToArray(array, length);
|
||||||
byte[] bytes = writeObjects(object);
|
byte[] bytes = writeObjects(object);
|
||||||
Object o = validate(bytes, object); // the object is its own filter
|
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
|
* @param value a test value
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="Limits")
|
@Test(dataProvider="Limits")
|
||||||
static void testLimits(String name, long value) {
|
void testLimits(String name, long value) {
|
||||||
Class<?> arrayClass = new int[0].getClass();
|
Class<?> arrayClass = new int[0].getClass();
|
||||||
String pattern = String.format("%s=%d;%s=%d", name, value, name, value - 1);
|
String pattern = String.format("%s=%d;%s=%d", name, value, name, value - 1);
|
||||||
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
|
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
|
||||||
|
@ -396,7 +399,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* @param pattern a pattern to test
|
* @param pattern a pattern to test
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="InvalidLimits", expectedExceptions=java.lang.IllegalArgumentException.class)
|
@Test(dataProvider="InvalidLimits", expectedExceptions=java.lang.IllegalArgumentException.class)
|
||||||
static void testInvalidLimits(String pattern) {
|
void testInvalidLimits(String pattern) {
|
||||||
try {
|
try {
|
||||||
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
|
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern);
|
||||||
} catch (IllegalArgumentException iae) {
|
} catch (IllegalArgumentException iae) {
|
||||||
|
@ -409,7 +412,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* Test that returning null from a filter causes deserialization to fail.
|
* Test that returning null from a filter causes deserialization to fail.
|
||||||
*/
|
*/
|
||||||
@Test(expectedExceptions=InvalidClassException.class)
|
@Test(expectedExceptions=InvalidClassException.class)
|
||||||
static void testNullStatus() throws IOException {
|
void testNullStatus() throws IOException {
|
||||||
byte[] bytes = writeObjects(0); // an Integer
|
byte[] bytes = writeObjects(0); // an Integer
|
||||||
try {
|
try {
|
||||||
Object o = validate(bytes, new ObjectInputFilter() {
|
Object o = validate(bytes, new ObjectInputFilter() {
|
||||||
|
@ -428,7 +431,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
* @param pattern pattern from the data source
|
* @param pattern pattern from the data source
|
||||||
*/
|
*/
|
||||||
@Test(dataProvider="InvalidPatterns", expectedExceptions=IllegalArgumentException.class)
|
@Test(dataProvider="InvalidPatterns", expectedExceptions=IllegalArgumentException.class)
|
||||||
static void testInvalidPatterns(String pattern) {
|
void testInvalidPatterns(String pattern) {
|
||||||
try {
|
try {
|
||||||
ObjectInputFilter.Config.createFilter(pattern);
|
ObjectInputFilter.Config.createFilter(pattern);
|
||||||
} catch (IllegalArgumentException iae) {
|
} 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 that Config.create returns null if the argument does not contain any patterns or limits.
|
||||||
*/
|
*/
|
||||||
@Test()
|
@Test()
|
||||||
static void testEmptyPattern() {
|
void testEmptyPattern() {
|
||||||
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("");
|
ObjectInputFilter filter = ObjectInputFilter.Config.createFilter("");
|
||||||
Assert.assertNull(filter, "empty pattern did not return null");
|
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.
|
* the ObjectInputFilter to check that it has the expected length.
|
||||||
*/
|
*/
|
||||||
static class ReadResolveToArray implements Serializable, ObjectInputFilter {
|
static class ReadResolveToArray implements Serializable, ObjectInputFilter {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 123456789L;
|
private static final long serialVersionUID = 123456789L;
|
||||||
|
|
||||||
@SuppressWarnings("serial") /* Incorrect declarations are being tested */
|
@SuppressWarnings("serial") /* Incorrect declarations are being tested */
|
||||||
|
@ -784,6 +788,7 @@ public class SerialFilterTest implements Serializable {
|
||||||
this.length = length;
|
this.length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serial
|
||||||
Object readResolve() {
|
Object readResolve() {
|
||||||
return array;
|
return array;
|
||||||
}
|
}
|
||||||
|
@ -844,21 +849,27 @@ public class SerialFilterTest implements Serializable {
|
||||||
|
|
||||||
// Deeper superclass hierarchy
|
// Deeper superclass hierarchy
|
||||||
static class A implements Serializable {
|
static class A implements Serializable {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 1L;
|
||||||
};
|
};
|
||||||
static class B extends A {
|
static class B extends A {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 2L;
|
private static final long serialVersionUID = 2L;
|
||||||
}
|
}
|
||||||
static class C extends B {
|
static class C extends B {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 3L;
|
private static final long serialVersionUID = 3L;
|
||||||
}
|
}
|
||||||
static class D extends C {
|
static class D extends C {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 4L;
|
private static final long serialVersionUID = 4L;
|
||||||
}
|
}
|
||||||
static class E extends D {
|
static class E extends D {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 5L;
|
private static final long serialVersionUID = 5L;
|
||||||
}
|
}
|
||||||
static class F extends E {
|
static class F extends E {
|
||||||
|
@Serial
|
||||||
private static final long serialVersionUID = 6L;
|
private static final long serialVersionUID = 6L;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# Deserialization Input Filter Factory
|
||||||
|
# See conf/security/java.security for pattern synatx
|
||||||
|
#
|
||||||
|
jdk.serialFilterFactory=SerialFilterFactoryTest$PropertyFilterFactory
|
Loading…
Add table
Add a link
Reference in a new issue