/* * 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. *

* 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. *

* 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. * *

* Typically, a custom filter should check if a process-wide filter * is configured and defer to it if so. For example, *

{@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;
 * }
 *}
*

* 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. *

* When setting the filter, it should be stateless and idempotent, * reporting the same result when passed the same arguments. *

* 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) () -> { 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. *

* 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. *

*

* 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. *

*

* 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, 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 = 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; } } } }