blob: 9b2c6891d69b6805045739d9b53ffa811b8c6071 [file] [log] [blame]
/*
* Copyright (c) 2016, 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* 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.
*/
package sun.misc;
import java.io.ObjectInputStream;
import java.io.SerializablePermission;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.security.Security;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Function;
import sun.util.logging.PlatformLogger;
/**
* Filter classes, array lengths, and graph metrics during deserialization.
* If set on an {@link ObjectInputStream}, the {@link #checkInput checkInput(FilterInfo)}
* method is called to validate classes, the length of each array,
* the number of objects being read from the stream, the depth of the graph,
* and the total number of bytes read from the stream.
* <p>
* A filter can be set via {@link ObjectInputStream#setObjectInputFilter setObjectInputFilter}
* for an individual ObjectInputStream.
* A filter can be set via {@link Config#setSerialFilter(ObjectInputFilter) Config.setSerialFilter}
* to affect every {@code ObjectInputStream} that does not otherwise set a filter.
* <p>
* A filter determines whether the arguments are {@link Status#ALLOWED ALLOWED}
* or {@link Status#REJECTED REJECTED} and should return the appropriate status.
* If the filter cannot determine the status it should return
* {@link Status#UNDECIDED UNDECIDED}.
* Filters should be designed for the specific use case and expected types.
* A filter designed for a particular use may be passed a class that is outside
* of the scope of the filter. If the purpose of the filter is to black-list classes
* then it can reject a candidate class that matches and report UNDECIDED for others.
* A filter may be called with class equals {@code null}, {@code arrayLength} equal -1,
* the depth, number of references, and stream size and return a status
* that reflects only one or only some of the values.
* This allows a filter to specific about the choice it is reporting and
* to use other filters without forcing either allowed or rejected status.
*
* <p>
* Typically, a custom filter should check if a process-wide filter
* is configured and defer to it if so. For example,
* <pre>{@code
* ObjectInputFilter.Status checkInput(FilterInfo info) {
* ObjectInputFilter serialFilter = ObjectInputFilter.Config.getSerialFilter();
* if (serialFilter != null) {
* ObjectInputFilter.Status status = serialFilter.checkInput(info);
* if (status != ObjectInputFilter.Status.UNDECIDED) {
* // The process-wide filter overrides this filter
* return status;
* }
* }
* if (info.serialClass() != null &&
* Remote.class.isAssignableFrom(info.serialClass())) {
* return Status.REJECTED; // Do not allow Remote objects
* }
* return Status.UNDECIDED;
* }
*}</pre>
* <p>
* Unless otherwise noted, passing a {@code null} argument to a
* method in this interface and its nested classes will cause a
* {@link NullPointerException} to be thrown.
*
* @since 8u
*/
@FunctionalInterface
public interface ObjectInputFilter {
/**
* Check the class, array length, number of object references, depth,
* stream size, and other available filtering information.
* Implementations of this method check the contents of the object graph being created
* during deserialization. The filter returns {@link Status#ALLOWED Status.ALLOWED},
* {@link Status#REJECTED Status.REJECTED}, or {@link Status#UNDECIDED Status.UNDECIDED}.
*
* @param filterInfo provides information about the current object being deserialized,
* if any, and the status of the {@link ObjectInputStream}
* @return {@link Status#ALLOWED Status.ALLOWED} if accepted,
* {@link Status#REJECTED Status.REJECTED} if rejected,
* {@link Status#UNDECIDED Status.UNDECIDED} if undecided.
*/
Status checkInput(FilterInfo filterInfo);
/**
* FilterInfo provides access to information about the current object
* being deserialized and the status of the {@link ObjectInputStream}.
* @since 9
*/
interface FilterInfo {
/**
* The class of an object being deserialized.
* For arrays, it is the array type.
* For example, the array class name of a 2 dimensional array of strings is
* "{@code [[Ljava.lang.String;}".
* To check the array's element type, iteratively use
* {@link Class#getComponentType() Class.getComponentType} while the result
* is an array and then check the class.
* The {@code serialClass is null} in the case where a new object is not being
* created and to give the filter a chance to check the depth, number of
* references to existing objects, and the stream size.
*
* @return class of an object being deserialized; may be null
*/
Class<?> serialClass();
/**
* The number of array elements when deserializing an array of the class.
*
* @return the non-negative number of array elements when deserializing
* an array of the class, otherwise -1
*/
long arrayLength();
/**
* The current depth.
* The depth starts at {@code 1} and increases for each nested object and
* decrements when each nested object returns.
*
* @return the current depth
*/
long depth();
/**
* The current number of object references.
*
* @return the non-negative current number of object references
*/
long references();
/**
* The current number of bytes consumed.
* @implSpec {@code streamBytes} is implementation specific
* and may not be directly related to the object in the stream
* that caused the callback.
*
* @return the non-negative current number of bytes consumed
*/
long streamBytes();
}
/**
* The status of a check on the class, array length, number of references,
* depth, and stream size.
*
* @since 8u
*/
enum Status {
/**
* The status is undecided, not allowed and not rejected.
*/
UNDECIDED,
/**
* The status is allowed.
*/
ALLOWED,
/**
* The status is rejected.
*/
REJECTED;
}
/**
* A utility class to set and get the process-wide filter or create a filter
* from a pattern string. If a process-wide filter is set, it will be
* used for each {@link ObjectInputStream} that does not set its own filter.
* <p>
* When setting the filter, it should be stateless and idempotent,
* reporting the same result when passed the same arguments.
* <p>
* The filter is configured using the {@link java.security.Security}
* property {@code jdk.serialFilter} and can be overridden by
* the System property {@code jdk.serialFilter}.
*
* The syntax is the same as for the {@link #createFilter(String) createFilter} method.
*
* @since 8u
*/
final class Config {
/* No instances. */
private Config() {}
/**
* Lock object for process-wide filter.
*/
private final static Object serialFilterLock = new Object();
/**
* Debug: Logger
*/
private final static PlatformLogger configLog;
/**
* Logger for debugging.
*/
static void filterLog(PlatformLogger.Level level, String msg, Object... args) {
if (configLog != null) {
if (PlatformLogger.Level.INFO.equals(level)) {
configLog.info(msg, args);
} else if (PlatformLogger.Level.WARNING.equals(level)) {
configLog.warning(msg, args);
} else {
configLog.severe(msg, args);
}
}
}
/**
* The name for the process-wide deserialization filter.
* Used as a system property and a java.security.Security property.
*/
private final static String SERIAL_FILTER_PROPNAME = "jdk.serialFilter";
/**
* The process-wide filter; may be null.
* Lookup the filter in java.security.Security or
* the system property.
*/
private final static ObjectInputFilter configuredFilter;
static {
configuredFilter = AccessController
.doPrivileged((PrivilegedAction<ObjectInputFilter>) () -> {
String props = System.getProperty(SERIAL_FILTER_PROPNAME);
if (props == null) {
props = Security.getProperty(SERIAL_FILTER_PROPNAME);
}
if (props != null) {
PlatformLogger log = PlatformLogger.getLogger("java.io.serialization");
log.info("Creating serialization filter from {0}", props);
try {
return createFilter(props);
} catch (RuntimeException re) {
log.warning("Error configuring filter: {0}", re);
}
}
return null;
});
configLog = (configuredFilter != null) ? PlatformLogger.getLogger("java.io.serialization") : null;
}
/**
* Current configured filter.
*/
private static ObjectInputFilter serialFilter = configuredFilter;
/**
* Get the filter for classes being deserialized on the ObjectInputStream.
*
* @param inputStream ObjectInputStream from which to get the filter; non-null
* @throws RuntimeException if the filter rejects
*/
public static ObjectInputFilter getObjectInputFilter(ObjectInputStream inputStream) {
Objects.requireNonNull(inputStream, "inputStream");
return sun.misc.SharedSecrets.getJavaOISAccess().getObjectInputFilter(inputStream);
}
/**
* Set the process-wide filter if it has not already been configured or set.
*
* @param inputStream ObjectInputStream on which to set the filter; non-null
* @param filter the serialization filter to set as the process-wide filter; not null
* @throws SecurityException if there is security manager and the
* {@code SerializablePermission("serialFilter")} is not granted
* @throws IllegalStateException if the filter has already been set {@code non-null}
*/
public static void setObjectInputFilter(ObjectInputStream inputStream,
ObjectInputFilter filter) {
Objects.requireNonNull(inputStream, "inputStream");
sun.misc.SharedSecrets.getJavaOISAccess().setObjectInputFilter(inputStream, filter);
}
/**
* Returns the process-wide serialization filter or {@code null} if not configured.
*
* @return the process-wide serialization filter or {@code null} if not configured
*/
public static ObjectInputFilter getSerialFilter() {
synchronized (serialFilterLock) {
return serialFilter;
}
}
/**
* Set the process-wide filter if it has not already been configured or set.
*
* @param filter the serialization filter to set as the process-wide filter; not null
* @throws SecurityException if there is security manager and the
* {@code SerializablePermission("serialFilter")} is not granted
* @throws IllegalStateException if the filter has already been set {@code non-null}
*/
public static void setSerialFilter(ObjectInputFilter filter) {
Objects.requireNonNull(filter, "filter");
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new SerializablePermission("serialFilter"));
}
synchronized (serialFilterLock) {
if (serialFilter != null) {
throw new IllegalStateException("Serial filter can only be set once");
}
serialFilter = filter;
}
}
/**
* Returns an ObjectInputFilter from a string of patterns.
* <p>
* Patterns are separated by ";" (semicolon). Whitespace is significant and
* is considered part of the pattern.
* If a pattern includes an equals assignment, "{@code =}" it sets a limit.
* If a limit appears more than once the last value is used.
* <ul>
* <li>maxdepth={@code value} - the maximum depth of a graph</li>
* <li>maxrefs={@code value} - the maximum number of internal references</li>
* <li>maxbytes={@code value} - the maximum number of bytes in the input stream</li>
* <li>maxarray={@code value} - the maximum array length allowed</li>
* </ul>
* <p>
* Other patterns match or reject class or package name
* as returned from {@link Class#getName() Class.getName()}.
* Note that for arrays the element type is used in the pattern,
* not the array type.
* <ul>
* <li>If the pattern starts with "!", the class is rejected if the remaining pattern is matched;
* otherwise the class is allowed if the pattern matches.
* <li>If the pattern ends with ".**" it matches any class in the package and all subpackages.
* <li>If the pattern ends with ".*" it matches any class in the package.
* <li>If the pattern ends with "*", it matches any class with the pattern as a prefix.
* <li>If the pattern is equal to the class name, it matches.
* <li>Otherwise, the pattern is not matched.
* </ul>
* <p>
* The resulting filter performs the limit checks and then
* tries to match the class, if any. If any of the limits are exceeded,
* the filter returns {@link Status#REJECTED Status.REJECTED}.
* If the class is an array type, the class to be matched is the element type.
* Arrays of any number of dimensions are treated the same as the element type.
* For example, a pattern of "{@code !example.Foo}",
* rejects creation of any instance or array of {@code example.Foo}.
* The first pattern that matches, working from left to right, determines
* the {@link Status#ALLOWED Status.ALLOWED}
* or {@link Status#REJECTED Status.REJECTED} result.
* If nothing matches, the result is {@link Status#UNDECIDED Status.UNDECIDED}.
*
* @param pattern the pattern string to parse; not null
* @return a filter to check a class being deserialized; may be null;
* {@code null} if no patterns
* @throws IllegalArgumentException
* if a limit is missing the name, or the long value
* is not a number or is negative,
* or if the package is missing for ".*" and ".**"
*/
public static ObjectInputFilter createFilter(String pattern) {
Objects.requireNonNull(pattern, "pattern");
return Global.createFilter(pattern, true);
}
/**
* Returns an ObjectInputFilter from a string of patterns that
* checks only the length for arrays, not the component type.
*
* @param pattern the pattern string to parse; not null
* @return a filter to check a class being deserialized;
* {@code null} if no patterns
*/
public static ObjectInputFilter createFilter2(String pattern) {
Objects.requireNonNull(pattern, "pattern");
return Global.createFilter(pattern, false);
}
/**
* Implementation of ObjectInputFilter that performs the checks of
* the process-wide serialization filter. If configured, it will be
* used for all ObjectInputStreams that do not set their own filters.
*
*/
final static class Global implements ObjectInputFilter {
/**
* The pattern used to create the filter.
*/
private final String pattern;
/**
* The list of class filters.
*/
private final List<Function<Class<?>, Status>> filters;
/**
* Maximum allowed bytes in the stream.
*/
private long maxStreamBytes;
/**
* Maximum depth of the graph allowed.
*/
private long maxDepth;
/**
* Maximum number of references in a graph.
*/
private long maxReferences;
/**
* Maximum length of any array.
*/
private long maxArrayLength;
/**
* True to check the component type for arrays.
*/
private final boolean checkComponentType;
/**
* Returns an ObjectInputFilter from a string of patterns.
*
* @param pattern the pattern string to parse
* @param checkComponentType true if the filter should check
* the component type of arrays
* @return a filter to check a class being deserialized; not null
* @throws IllegalArgumentException if the parameter is malformed
* if the pattern is missing the name, the long value
* is not a number or is negative.
*/
static ObjectInputFilter createFilter(String pattern, boolean checkComponentType) {
Global filter = new Global(pattern, checkComponentType);
return filter.isEmpty() ? null : filter;
}
/**
* Construct a new filter from the pattern String.
*
* @param pattern a pattern string of filters
* @param checkComponentType true if the filter should check
* the component type of arrays
* @throws IllegalArgumentException if the pattern is malformed
*/
private Global(String pattern, boolean checkComponentType) {
this.pattern = pattern;
this.checkComponentType = checkComponentType;
maxArrayLength = Long.MAX_VALUE; // Default values are unlimited
maxDepth = Long.MAX_VALUE;
maxReferences = Long.MAX_VALUE;
maxStreamBytes = Long.MAX_VALUE;
String[] patterns = pattern.split(";");
filters = new ArrayList<>(patterns.length);
for (int i = 0; i < patterns.length; i++) {
String p = patterns[i];
int nameLen = p.length();
if (nameLen == 0) {
continue;
}
if (parseLimit(p)) {
// If the pattern contained a limit setting, i.e. type=value
continue;
}
boolean negate = p.charAt(0) == '!';
if (p.indexOf('/') >= 0) {
throw new IllegalArgumentException("invalid character \"/\" in: \"" + pattern + "\"");
}
if (p.endsWith("*")) {
// Wildcard cases
if (p.endsWith(".*")) {
// Pattern is a package name with a wildcard
final String pkg = p.substring(negate ? 1 : 0, nameLen - 1);
if (pkg.length() < 2) {
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
// A Function that fails if the class starts with the pattern, otherwise don't care
filters.add(c -> matchesPackage(c, pkg) ? Status.REJECTED : Status.UNDECIDED);
} else {
// A Function that succeeds if the class starts with the pattern, otherwise don't care
filters.add(c -> matchesPackage(c, pkg) ? Status.ALLOWED : Status.UNDECIDED);
}
} else if (p.endsWith(".**")) {
// Pattern is a package prefix with a double wildcard
final String pkgs = p.substring(negate ? 1 : 0, nameLen - 2);
if (pkgs.length() < 2) {
throw new IllegalArgumentException("package missing in: \"" + pattern + "\"");
}
if (negate) {
// A Function that fails if the class starts with the pattern, otherwise don't care
filters.add(c -> c.getName().startsWith(pkgs) ? Status.REJECTED : Status.UNDECIDED);
} else {
// A Function that succeeds if the class starts with the pattern, otherwise don't care
filters.add(c -> c.getName().startsWith(pkgs) ? Status.ALLOWED : Status.UNDECIDED);
}
} else {
// Pattern is a classname (possibly empty) with a trailing wildcard
final String className = p.substring(negate ? 1 : 0, nameLen - 1);
if (negate) {
// A Function that fails if the class starts with the pattern, otherwise don't care
filters.add(c -> c.getName().startsWith(className) ? Status.REJECTED : Status.UNDECIDED);
} else {
// A Function that succeeds if the class starts with the pattern, otherwise don't care
filters.add(c -> c.getName().startsWith(className) ? Status.ALLOWED : Status.UNDECIDED);
}
}
} else {
final String name = p.substring(negate ? 1 : 0);
if (name.isEmpty()) {
throw new IllegalArgumentException("class or package missing in: \"" + pattern + "\"");
}
// Pattern is a class name
if (negate) {
// A Function that fails if the class equals the pattern, otherwise don't care
filters.add(c -> c.getName().equals(name) ? Status.REJECTED : Status.UNDECIDED);
} else {
// A Function that succeeds if the class equals the pattern, otherwise don't care
filters.add(c -> c.getName().equals(name) ? Status.ALLOWED : Status.UNDECIDED);
}
}
}
}
/**
* Returns if this filter has any checks.
* @return {@code true} if the filter has any checks, {@code false} otherwise
*/
private boolean isEmpty() {
return filters.isEmpty() &&
maxArrayLength == Long.MAX_VALUE &&
maxDepth == Long.MAX_VALUE &&
maxReferences == Long.MAX_VALUE &&
maxStreamBytes == Long.MAX_VALUE;
}
/**
* Parse out a limit for one of maxarray, maxdepth, maxbytes, maxreferences.
*
* @param pattern a string with a type name, '=' and a value
* @return {@code true} if a limit was parsed, else {@code false}
* @throws IllegalArgumentException if the pattern is missing
* the name, the Long value is not a number or is negative.
*/
private boolean parseLimit(String pattern) {
int eqNdx = pattern.indexOf('=');
if (eqNdx < 0) {
// not a limit pattern
return false;
}
String valueString = pattern.substring(eqNdx + 1);
if (pattern.startsWith("maxdepth=")) {
maxDepth = parseValue(valueString);
} else if (pattern.startsWith("maxarray=")) {
maxArrayLength = parseValue(valueString);
} else if (pattern.startsWith("maxrefs=")) {
maxReferences = parseValue(valueString);
} else if (pattern.startsWith("maxbytes=")) {
maxStreamBytes = parseValue(valueString);
} else {
throw new IllegalArgumentException("unknown limit: " + pattern.substring(0, eqNdx));
}
return true;
}
/**
* Parse the value of a limit and check that it is non-negative.
* @param string inputstring
* @return the parsed value
* @throws IllegalArgumentException if parsing the value fails or the value is negative
*/
private static long parseValue(String string) throws IllegalArgumentException {
// Parse a Long from after the '=' to the end
long value = Long.parseLong(string);
if (value < 0) {
throw new IllegalArgumentException("negative limit: " + string);
}
return value;
}
/**
* {@inheritDoc}
*/
@Override
public Status checkInput(FilterInfo filterInfo) {
if (filterInfo.references() < 0
|| filterInfo.depth() < 0
|| filterInfo.streamBytes() < 0
|| filterInfo.references() > maxReferences
|| filterInfo.depth() > maxDepth
|| filterInfo.streamBytes() > maxStreamBytes) {
return Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
if (clazz.isArray()) {
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > maxArrayLength) {
// array length is too big
return Status.REJECTED;
}
if (!checkComponentType) {
// As revised; do not check the component type for arrays
return Status.UNDECIDED;
}
do {
// Arrays are decided based on the component type
clazz = clazz.getComponentType();
} while (clazz.isArray());
}
if (clazz.isPrimitive()) {
// Primitive types are undecided; let someone else decide
return Status.UNDECIDED;
} else {
// Find any filter that allowed or rejected the class
final Class<?> cl = clazz;
Optional<Status> status = filters.stream()
.map(f -> f.apply(cl))
.filter(p -> p != Status.UNDECIDED)
.findFirst();
return status.orElse(Status.UNDECIDED);
}
}
return Status.UNDECIDED;
}
/**
* Returns {@code true} if the class is in the package.
*
* @param c a class
* @param pkg a package name (including the trailing ".")
* @return {@code true} if the class is in the package,
* otherwise {@code false}
*/
private static boolean matchesPackage(Class<?> c, String pkg) {
String n = c.getName();
return n.startsWith(pkg) && n.lastIndexOf('.') == pkg.length() - 1;
}
/**
* Returns the pattern used to create this filter.
* @return the pattern used to create this filter
*/
@Override
public String toString() {
return pattern;
}
}
}
}