From 46d40140d384008b7896da69167e79bb60ed4ed5 Mon Sep 17 00:00:00 2001 From: rriggs Date: Tue, 4 Oct 2016 14:52:57 -0400 Subject: [PATCH] 8160108: Implement Serialization Filtering 8166739: Improve extensibility of ObjectInputFilter information passed to the filter Reviewed-by: dfuchs, ahgross, chegar, skoivu --- .../classes/java/io/ObjectInputStream.java | 325 +++++++- src/share/classes/sun/misc/JavaOISAccess.java | 33 + .../classes/sun/misc/ObjectInputFilter.java | 642 +++++++++++++++ src/share/classes/sun/misc/SharedSecrets.java | 14 + src/share/lib/security/java.security-aix | 37 + src/share/lib/security/java.security-linux | 37 + src/share/lib/security/java.security-macosx | 37 + src/share/lib/security/java.security-solaris | 37 + src/share/lib/security/java.security-windows | 37 + .../serialFilter/CheckInputOrderTest.java | 94 +++ .../FilterWithSecurityManagerTest.java | 101 +++ .../serialFilter/GlobalFilterTest.java | 219 ++++++ .../serialFilter/MixedFiltersTest.java | 138 ++++ .../serialFilter/SerialFilterTest.java | 743 ++++++++++++++++++ .../serialFilter/java.security-extra1 | 4 + .../Serializable/serialFilter/security.policy | 17 + .../security.policy.without.globalFilter | 15 + 17 files changed, 2510 insertions(+), 20 deletions(-) create mode 100644 src/share/classes/sun/misc/JavaOISAccess.java create mode 100644 src/share/classes/sun/misc/ObjectInputFilter.java create mode 100644 test/java/io/Serializable/serialFilter/CheckInputOrderTest.java create mode 100644 test/java/io/Serializable/serialFilter/FilterWithSecurityManagerTest.java create mode 100644 test/java/io/Serializable/serialFilter/GlobalFilterTest.java create mode 100644 test/java/io/Serializable/serialFilter/MixedFiltersTest.java create mode 100644 test/java/io/Serializable/serialFilter/SerialFilterTest.java create mode 100644 test/java/io/Serializable/serialFilter/java.security-extra1 create mode 100644 test/java/io/Serializable/serialFilter/security.policy create mode 100644 test/java/io/Serializable/serialFilter/security.policy.without.globalFilter diff --git a/src/share/classes/java/io/ObjectInputStream.java b/src/share/classes/java/io/ObjectInputStream.java index 9da6182ea..b5cb8288d 100644 --- a/src/share/classes/java/io/ObjectInputStream.java +++ b/src/share/classes/java/io/ObjectInputStream.java @@ -37,13 +37,18 @@ import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.util.Arrays; import java.util.HashMap; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; + import static java.io.ObjectStreamClass.processQueue; + +import sun.misc.ObjectInputFilter; import sun.misc.ObjectStreamClassValidator; import sun.misc.SharedSecrets; -import sun.misc.Unsafe; import sun.reflect.misc.ReflectUtil; +import sun.misc.JavaOISAccess; +import sun.util.logging.PlatformLogger; /** * An ObjectInputStream deserializes primitive data and objects previously @@ -239,12 +244,48 @@ public class ObjectInputStream new ReferenceQueue<>(); } + static { + /* Setup access so sun.misc can invoke package private functions. */ + sun.misc.SharedSecrets.setJavaOISAccess(new JavaOISAccess() { + public void setObjectInputFilter(ObjectInputStream stream, ObjectInputFilter filter) { + stream.setInternalObjectInputFilter(filter); + } + + public ObjectInputFilter getObjectInputFilter(ObjectInputStream stream) { + return stream.getInternalObjectInputFilter(); + } + }); + } + + /* + * Separate class to defer initialization of logging until needed. + */ + private static class Logging { + + /* + * Logger for ObjectInputFilter results. + * Setup the filter logger if it is set to INFO or WARNING. + * (Assuming it will not change). + */ + private static final PlatformLogger traceLogger; + private static final PlatformLogger infoLogger; + static { + PlatformLogger filterLog = PlatformLogger.getLogger("java.io.serialization"); + infoLogger = (filterLog != null && + filterLog.isLoggable(PlatformLogger.Level.INFO)) ? filterLog : null; + traceLogger = (filterLog != null && + filterLog.isLoggable(PlatformLogger.Level.FINER)) ? filterLog : null; + } + } + /** filter stream for handling block data conversion */ private final BlockDataInputStream bin; /** validation callback list */ private final ValidationList vlist; /** recursion depth */ - private int depth; + private long depth; + /** Total number of references to any type of object, class, enum, proxy, etc. */ + private long totalObjectRefs; /** whether stream is closed */ private boolean closed; @@ -270,6 +311,12 @@ public class ObjectInputStream */ private SerialCallbackContext curContext; + /** + * Filter of class descriptors and classes read from the stream; + * may be null. + */ + private ObjectInputFilter serialFilter; + /** * Creates an ObjectInputStream that reads from the specified InputStream. * A serialization stream header is read from the stream and verified. @@ -297,6 +344,7 @@ public class ObjectInputStream bin = new BlockDataInputStream(in); handles = new HandleTable(10); vlist = new ValidationList(); + serialFilter = ObjectInputFilter.Config.getSerialFilter(); enableOverride = false; readStreamHeader(); bin.setBlockDataMode(true); @@ -327,6 +375,7 @@ public class ObjectInputStream bin = null; handles = null; vlist = null; + serialFilter = ObjectInputFilter.Config.getSerialFilter(); enableOverride = true; } @@ -334,7 +383,7 @@ public class ObjectInputStream * Read an object from the ObjectInputStream. The class of the object, the * signature of the class, and the values of the non-transient and * non-static fields of the class and all of its supertypes are read. - * Default deserializing for a class can be overriden using the writeObject + * Default deserializing for a class can be overridden using the writeObject * and readObject methods. Objects referenced by this object are read * transitively so that a complete equivalent graph of objects is * reconstructed by readObject. @@ -1075,6 +1124,138 @@ public class ObjectInputStream return bin.readUTF(); } + /** + * Returns the serialization filter for this stream. + * The serialization filter is the most recent filter set in + * {@link #setInternalObjectInputFilter setInternalObjectInputFilter} or + * the initial process-wide filter from + * {@link ObjectInputFilter.Config#getSerialFilter() ObjectInputFilter.Config.getSerialFilter}. + * + * @return the serialization filter for the stream; may be null + */ + private final ObjectInputFilter getInternalObjectInputFilter() { + return serialFilter; + } + + /** + * Set the serialization filter for the stream. + * The filter's {@link ObjectInputFilter#checkInput checkInput} method is called + * for each class and reference in the stream. + * The filter can check any or all of the class, the array length, the number + * of references, the depth of the graph, and the size of the input stream. + *

+ * If the filter returns {@link ObjectInputFilter.Status#REJECTED Status.REJECTED}, + * {@code null} or throws a {@link RuntimeException}, + * the active {@code readObject} or {@code readUnshared} + * throws {@link InvalidClassException}, otherwise deserialization + * continues uninterrupted. + *

+ * 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. + * + * @implSpec + * The filter, when not {@code null}, is invoked during {@link #readObject readObject} + * and {@link #readUnshared readUnshared} for each object + * (regular or class) in the stream including the following: + *

+ * + * When the {@link ObjectInputFilter#checkInput checkInput} method is invoked + * it is given access to the current class, the array length, + * the current number of references already read from the stream, + * the depth of nested calls to {@link #readObject readObject} or + * {@link #readUnshared readUnshared}, + * and the implementation dependent number of bytes consumed from the input stream. + *

+ * Each call to {@link #readObject readObject} or + * {@link #readUnshared readUnshared} increases the depth by 1 + * before reading an object and decreases by 1 before returning + * normally or exceptionally. + * The depth starts at {@code 1} and increases for each nested object and + * decrements when each nested call returns. + * The count of references in the stream starts at {@code 1} and + * is increased before reading an object. + * + * @param filter the filter, may be null + * @throws SecurityException if there is security manager and the + * {@code SerializablePermission("serialFilter")} is not granted + * @throws IllegalStateException if the {@linkplain #getInternalObjectInputFilter() current filter} + * is not {@code null} and is not the process-wide filter + */ + private final void setInternalObjectInputFilter(ObjectInputFilter filter) { + SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new SerializablePermission("serialFilter")); + } + // Allow replacement of the process-wide filter if not already set + if (serialFilter != null && + serialFilter != ObjectInputFilter.Config.getSerialFilter()) { + throw new IllegalStateException("filter can not be set more than once"); + } + this.serialFilter = filter; + } + + /** + * Invoke the serialization filter if non-null. + * If the filter rejects or an exception is thrown, throws InvalidClassException. + * + * @param clazz the class; may be null + * @param arrayLength the array length requested; use {@code -1} if not creating an array + * @throws InvalidClassException if it rejected by the filter or + * a {@link RuntimeException} is thrown + */ + private void filterCheck(Class clazz, int arrayLength) + throws InvalidClassException { + if (serialFilter != null) { + RuntimeException ex = null; + ObjectInputFilter.Status status; + try { + status = serialFilter.checkInput(new FilterValues(clazz, arrayLength, + totalObjectRefs, depth, bin.getBytesRead())); + } catch (RuntimeException e) { + // Preventive interception of an exception to log + status = ObjectInputFilter.Status.REJECTED; + ex = e; + } + if (status == null || + status == ObjectInputFilter.Status.REJECTED) { + // Debug logging of filter checks that fail + if (Logging.infoLogger != null) { + Logging.infoLogger.info( + "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}", + status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(), + Objects.toString(ex, "n/a")); + } + InvalidClassException ice = new InvalidClassException("filter status: " + status); + ice.initCause(ex); + throw ice; + } else { + // Trace logging for those that succeed + if (Logging.traceLogger != null) { + Logging.traceLogger.finer( + "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}", + status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(), + Objects.toString(ex, "n/a")); + } + } + } + } + /** * Provide access to the persistent fields read from the input stream. */ @@ -1324,6 +1505,7 @@ public class ObjectInputStream } depth++; + totalObjectRefs++; try { switch (tc) { case TC_NULL: @@ -1400,6 +1582,15 @@ public class ObjectInputStream } Object rep = resolveObject(obj); if (rep != obj) { + // The type of the original object has been filtered but resolveObject + // may have replaced it; filter the replacement's type + if (rep != null) { + if (rep.getClass().isArray()) { + filterCheck(rep.getClass(), Array.getLength(rep)); + } else { + filterCheck(rep.getClass(), -1); + } + } handles.setObject(passHandle, rep); } return rep; @@ -1470,6 +1661,7 @@ public class ObjectInputStream throw new InvalidObjectException( "cannot read back reference to unshared object"); } + filterCheck(null, -1); // just a check for number of references, depth, no class return obj; } @@ -1574,6 +1766,10 @@ public class ObjectInputStream ReflectUtil.checkProxyPackageAccess( getClass().getClassLoader(), cl.getInterfaces()); + // Filter the interfaces + for (Class clazz : cl.getInterfaces()) { + filterCheck(clazz, -1); + } } } catch (ClassNotFoundException ex) { resolveEx = ex; @@ -1582,6 +1778,9 @@ public class ObjectInputStream desc.initProxy(cl, resolveEx, readClassDesc(false)); + // Call filterCheck on the definition + filterCheck(desc.forClass(), -1); + handles.finish(descHandle); passHandle = descHandle; return desc; @@ -1629,8 +1828,12 @@ public class ObjectInputStream desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false)); + // Call filterCheck on the definition + filterCheck(desc.forClass(), -1); + handles.finish(descHandle); passHandle = descHandle; + return desc; } @@ -1671,6 +1874,8 @@ public class ObjectInputStream ObjectStreamClass desc = readClassDesc(false); int len = bin.readInt(); + filterCheck(desc.forClass(), len); + Object array = null; Class cl, ccl = null; if ((cl = desc.forClass()) != null) { @@ -1819,6 +2024,14 @@ public class ObjectInputStream rep = cloneArray(rep); } if (rep != obj) { + // Filter the replacement object + if (rep != null) { + if (rep.getClass().isArray()) { + filterCheck(rep.getClass(), Array.getLength(rep)); + } else { + filterCheck(rep.getClass(), -1); + } + } handles.setObject(passHandle, obj = rep); } } @@ -2009,22 +2222,22 @@ public class ObjectInputStream desc.setPrimFieldValues(obj, primVals); } - int objHandle = passHandle; - ObjectStreamField[] fields = desc.getFields(false); + int objHandle = passHandle; + ObjectStreamField[] fields = desc.getFields(false); Object[] objVals = new Object[desc.getNumObjFields()]; - int numPrimFields = fields.length - objVals.length; - for (int i = 0; i < objVals.length; i++) { - ObjectStreamField f = fields[numPrimFields + i]; - objVals[i] = readObject0(f.isUnshared()); - if (f.getField() != null) { - handles.markDependency(objHandle, passHandle); - } + int numPrimFields = fields.length - objVals.length; + for (int i = 0; i < objVals.length; i++) { + ObjectStreamField f = fields[numPrimFields + i]; + objVals[i] = readObject0(f.isUnshared()); + if (f.getField() != null) { + handles.markDependency(objHandle, passHandle); } + } if (obj != null) { desc.setObjFieldValues(obj, objVals); } - passHandle = objHandle; - } + passHandle = objHandle; + } /** * Reads in and returns IOException that caused serialization to abort. @@ -2296,6 +2509,51 @@ public class ObjectInputStream } } + /** + * Hold a snapshot of values to be passed to an ObjectInputFilter. + */ + static class FilterValues implements ObjectInputFilter.FilterInfo { + final Class clazz; + final long arrayLength; + final long totalObjectRefs; + final long depth; + final long streamBytes; + + public FilterValues(Class clazz, long arrayLength, long totalObjectRefs, + long depth, long streamBytes) { + this.clazz = clazz; + this.arrayLength = arrayLength; + this.totalObjectRefs = totalObjectRefs; + this.depth = depth; + this.streamBytes = streamBytes; + } + + @Override + public Class serialClass() { + return clazz; + } + + @Override + public long arrayLength() { + return arrayLength; + } + + @Override + public long references() { + return totalObjectRefs; + } + + @Override + public long depth() { + return depth; + } + + @Override + public long streamBytes() { + return streamBytes; + } + } + /** * Input stream supporting single-byte peek operations. */ @@ -2305,6 +2563,8 @@ public class ObjectInputStream private final InputStream in; /** peeked byte */ private int peekb = -1; + /** total bytes read from the stream */ + private long totalBytesRead = 0; /** * Creates new PeekInputStream on top of given underlying stream. @@ -2318,7 +2578,12 @@ public class ObjectInputStream * that it does not consume the read value. */ int peek() throws IOException { - return (peekb >= 0) ? peekb : (peekb = in.read()); + if (peekb >= 0) { + return peekb; + } + peekb = in.read(); + totalBytesRead += peekb >= 0 ? 1 : 0; + return peekb; } public int read() throws IOException { @@ -2327,21 +2592,27 @@ public class ObjectInputStream peekb = -1; return v; } else { - return in.read(); + int nbytes = in.read(); + totalBytesRead += nbytes >= 0 ? 1 : 0; + return nbytes; } } public int read(byte[] b, int off, int len) throws IOException { + int nbytes; if (len == 0) { return 0; } else if (peekb < 0) { - return in.read(b, off, len); + nbytes = in.read(b, off, len); + totalBytesRead += nbytes >= 0 ? nbytes : 0; + return nbytes; } else { b[off++] = (byte) peekb; len--; peekb = -1; - int n = in.read(b, off, len); - return (n >= 0) ? (n + 1) : 1; + nbytes = in.read(b, off, len); + totalBytesRead += nbytes >= 0 ? nbytes : 0; + return (nbytes >= 0) ? (nbytes + 1) : 1; } } @@ -2366,7 +2637,9 @@ public class ObjectInputStream skipped++; n--; } - return skipped + skip(n); + n = skipped + in.skip(n); + totalBytesRead += n; + return n; } public int available() throws IOException { @@ -2376,6 +2649,10 @@ public class ObjectInputStream public void close() throws IOException { in.close(); } + + public long getBytesRead() { + return totalBytesRead; + } } /** @@ -3231,6 +3508,14 @@ public class ObjectInputStream throw new UTFDataFormatException(); } } + + /** + * Returns the number of bytes read from the input stream. + * @return the number of bytes read from the input stream + */ + long getBytesRead() { + return in.getBytesRead(); + } } /** diff --git a/src/share/classes/sun/misc/JavaOISAccess.java b/src/share/classes/sun/misc/JavaOISAccess.java new file mode 100644 index 000000000..8be96eb7c --- /dev/null +++ b/src/share/classes/sun/misc/JavaOISAccess.java @@ -0,0 +1,33 @@ +/* + * 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; + +public interface JavaOISAccess { + void setObjectInputFilter(ObjectInputStream stream, ObjectInputFilter filter); + ObjectInputFilter getObjectInputFilter(ObjectInputStream stream); +} diff --git a/src/share/classes/sun/misc/ObjectInputFilter.java b/src/share/classes/sun/misc/ObjectInputFilter.java new file mode 100644 index 000000000..467f7b709 --- /dev/null +++ b/src/share/classes/sun/misc/ObjectInputFilter.java @@ -0,0 +1,642 @@ +/* + * 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); + } + + /** + * 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; + + /** + * Returns an ObjectInputFilter from a string of patterns. + * + * @param pattern the pattern string to parse + * @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) { + Global filter = new Global(pattern); + return filter.isEmpty() ? null : filter; + } + + /** + * Construct a new filter from the pattern String. + * + * @param pattern a pattern string of filters + * @throws IllegalArgumentException if the pattern is malformed + */ + private Global(String pattern) { + this.pattern = pattern; + + 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; + } + 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; + } + } + } +} diff --git a/src/share/classes/sun/misc/SharedSecrets.java b/src/share/classes/sun/misc/SharedSecrets.java index 2f995dab4..3539962ba 100644 --- a/src/share/classes/sun/misc/SharedSecrets.java +++ b/src/share/classes/sun/misc/SharedSecrets.java @@ -25,6 +25,7 @@ package sun.misc; +import java.io.ObjectInputStream; import java.util.jar.JarFile; import java.io.Console; import java.io.FileDescriptor; @@ -56,6 +57,7 @@ public class SharedSecrets { private static JavaSecurityAccess javaSecurityAccess; private static JavaUtilZipFileAccess javaUtilZipFileAccess; private static JavaAWTAccess javaAWTAccess; + private static JavaOISAccess javaOISAccess; private static JavaObjectInputStreamAccess javaObjectInputStreamAccess; public static JavaUtilJarAccess javaUtilJarAccess() { @@ -141,6 +143,18 @@ public class SharedSecrets { return javaIOFileDescriptorAccess; } + public static void setJavaOISAccess(JavaOISAccess access) { + javaOISAccess = access; + } + + public static JavaOISAccess getJavaOISAccess() { + if (javaOISAccess == null) + unsafe.ensureClassInitialized(ObjectInputStream.class); + + return javaOISAccess; + } + + public static void setJavaSecurityProtectionDomainAccess (JavaSecurityProtectionDomainAccess jspda) { javaSecurityProtectionDomainAccess = jspda; diff --git a/src/share/lib/security/java.security-aix b/src/share/lib/security/java.security-aix index a47daa258..d28d746cb 100644 --- a/src/share/lib/security/java.security-aix +++ b/src/share/lib/security/java.security-aix @@ -700,3 +700,40 @@ jdk.tls.legacyAlgorithms= \ # implementations. # jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024 + +# +# Serialization process-wide filter +# +# A filter, if configured, is used by java.io.ObjectInputStream during +# deserialization to check the contents of the stream. +# A filter is configured as a sequence of patterns, each pattern is either +# matched against the name of a class in the stream or defines a limit. +# Patterns are separated by ";" (semicolon). +# Whitespace is significant and is considered part of the pattern. +# +# If a pattern includes a "=", it sets a limit. +# If a limit appears more than once the last value is used. +# Limits are checked before classes regardless of the order in the sequence of patterns. +# If any of the limits are exceeded, the filter status is REJECTED. +# +# maxdepth=value - the maximum depth of a graph +# maxrefs=value - the maximum number of internal references +# maxbytes=value - the maximum number of bytes in the input stream +# maxarray=value - the maximum array length allowed +# +# Other patterns, from left to right, match the class or package name as +# returned from Class.getName. +# If the class is an array type, the class or package 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 "!example.Foo", rejects creation of any instance or +# array of example.Foo. +# +# If the pattern starts with "!", the status is REJECTED if the remaining pattern +# is matched; otherwise the status is ALLOWED if the pattern matches. +# If the pattern ends with ".**" it matches any class in the package and all subpackages. +# If the pattern ends with ".*" it matches any class in the package. +# If the pattern ends with "*", it matches any class with the pattern as a prefix. +# If the pattern is equal to the class name, it matches. +# Otherwise, the status is UNDECIDED. +# +#jdk.serialFilter=pattern;pattern diff --git a/src/share/lib/security/java.security-linux b/src/share/lib/security/java.security-linux index a47daa258..d28d746cb 100644 --- a/src/share/lib/security/java.security-linux +++ b/src/share/lib/security/java.security-linux @@ -700,3 +700,40 @@ jdk.tls.legacyAlgorithms= \ # implementations. # jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024 + +# +# Serialization process-wide filter +# +# A filter, if configured, is used by java.io.ObjectInputStream during +# deserialization to check the contents of the stream. +# A filter is configured as a sequence of patterns, each pattern is either +# matched against the name of a class in the stream or defines a limit. +# Patterns are separated by ";" (semicolon). +# Whitespace is significant and is considered part of the pattern. +# +# If a pattern includes a "=", it sets a limit. +# If a limit appears more than once the last value is used. +# Limits are checked before classes regardless of the order in the sequence of patterns. +# If any of the limits are exceeded, the filter status is REJECTED. +# +# maxdepth=value - the maximum depth of a graph +# maxrefs=value - the maximum number of internal references +# maxbytes=value - the maximum number of bytes in the input stream +# maxarray=value - the maximum array length allowed +# +# Other patterns, from left to right, match the class or package name as +# returned from Class.getName. +# If the class is an array type, the class or package 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 "!example.Foo", rejects creation of any instance or +# array of example.Foo. +# +# If the pattern starts with "!", the status is REJECTED if the remaining pattern +# is matched; otherwise the status is ALLOWED if the pattern matches. +# If the pattern ends with ".**" it matches any class in the package and all subpackages. +# If the pattern ends with ".*" it matches any class in the package. +# If the pattern ends with "*", it matches any class with the pattern as a prefix. +# If the pattern is equal to the class name, it matches. +# Otherwise, the status is UNDECIDED. +# +#jdk.serialFilter=pattern;pattern diff --git a/src/share/lib/security/java.security-macosx b/src/share/lib/security/java.security-macosx index ae29034fb..93c74b01d 100644 --- a/src/share/lib/security/java.security-macosx +++ b/src/share/lib/security/java.security-macosx @@ -703,3 +703,40 @@ jdk.tls.legacyAlgorithms= \ # implementations. # jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024 + +# +# Serialization process-wide filter +# +# A filter, if configured, is used by java.io.ObjectInputStream during +# deserialization to check the contents of the stream. +# A filter is configured as a sequence of patterns, each pattern is either +# matched against the name of a class in the stream or defines a limit. +# Patterns are separated by ";" (semicolon). +# Whitespace is significant and is considered part of the pattern. +# +# If a pattern includes a "=", it sets a limit. +# If a limit appears more than once the last value is used. +# Limits are checked before classes regardless of the order in the sequence of patterns. +# If any of the limits are exceeded, the filter status is REJECTED. +# +# maxdepth=value - the maximum depth of a graph +# maxrefs=value - the maximum number of internal references +# maxbytes=value - the maximum number of bytes in the input stream +# maxarray=value - the maximum array length allowed +# +# Other patterns, from left to right, match the class or package name as +# returned from Class.getName. +# If the class is an array type, the class or package 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 "!example.Foo", rejects creation of any instance or +# array of example.Foo. +# +# If the pattern starts with "!", the status is REJECTED if the remaining pattern +# is matched; otherwise the status is ALLOWED if the pattern matches. +# If the pattern ends with ".**" it matches any class in the package and all subpackages. +# If the pattern ends with ".*" it matches any class in the package. +# If the pattern ends with "*", it matches any class with the pattern as a prefix. +# If the pattern is equal to the class name, it matches. +# Otherwise, the status is UNDECIDED. +# +#jdk.serialFilter=pattern;pattern diff --git a/src/share/lib/security/java.security-solaris b/src/share/lib/security/java.security-solaris index 554496d53..29e9d952b 100644 --- a/src/share/lib/security/java.security-solaris +++ b/src/share/lib/security/java.security-solaris @@ -702,3 +702,40 @@ jdk.tls.legacyAlgorithms= \ # implementations. # jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024 + +# +# Serialization process-wide filter +# +# A filter, if configured, is used by java.io.ObjectInputStream during +# deserialization to check the contents of the stream. +# A filter is configured as a sequence of patterns, each pattern is either +# matched against the name of a class in the stream or defines a limit. +# Patterns are separated by ";" (semicolon). +# Whitespace is significant and is considered part of the pattern. +# +# If a pattern includes a "=", it sets a limit. +# If a limit appears more than once the last value is used. +# Limits are checked before classes regardless of the order in the sequence of patterns. +# If any of the limits are exceeded, the filter status is REJECTED. +# +# maxdepth=value - the maximum depth of a graph +# maxrefs=value - the maximum number of internal references +# maxbytes=value - the maximum number of bytes in the input stream +# maxarray=value - the maximum array length allowed +# +# Other patterns, from left to right, match the class or package name as +# returned from Class.getName. +# If the class is an array type, the class or package 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 "!example.Foo", rejects creation of any instance or +# array of example.Foo. +# +# If the pattern starts with "!", the status is REJECTED if the remaining pattern +# is matched; otherwise the status is ALLOWED if the pattern matches. +# If the pattern ends with ".**" it matches any class in the package and all subpackages. +# If the pattern ends with ".*" it matches any class in the package. +# If the pattern ends with "*", it matches any class with the pattern as a prefix. +# If the pattern is equal to the class name, it matches. +# Otherwise, the status is UNDECIDED. +# +#jdk.serialFilter=pattern;pattern diff --git a/src/share/lib/security/java.security-windows b/src/share/lib/security/java.security-windows index 62f48ba5a..bbe3dd0d6 100644 --- a/src/share/lib/security/java.security-windows +++ b/src/share/lib/security/java.security-windows @@ -703,3 +703,40 @@ jdk.tls.legacyAlgorithms= \ # implementations. # jdk.jar.disabledAlgorithms=MD2, RSA keySize < 1024 + +# +# Serialization process-wide filter +# +# A filter, if configured, is used by java.io.ObjectInputStream during +# deserialization to check the contents of the stream. +# A filter is configured as a sequence of patterns, each pattern is either +# matched against the name of a class in the stream or defines a limit. +# Patterns are separated by ";" (semicolon). +# Whitespace is significant and is considered part of the pattern. +# +# If a pattern includes a "=", it sets a limit. +# If a limit appears more than once the last value is used. +# Limits are checked before classes regardless of the order in the sequence of patterns. +# If any of the limits are exceeded, the filter status is REJECTED. +# +# maxdepth=value - the maximum depth of a graph +# maxrefs=value - the maximum number of internal references +# maxbytes=value - the maximum number of bytes in the input stream +# maxarray=value - the maximum array length allowed +# +# Other patterns, from left to right, match the class or package name as +# returned from Class.getName. +# If the class is an array type, the class or package 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 "!example.Foo", rejects creation of any instance or +# array of example.Foo. +# +# If the pattern starts with "!", the status is REJECTED if the remaining pattern +# is matched; otherwise the status is ALLOWED if the pattern matches. +# If the pattern ends with ".**" it matches any class in the package and all subpackages. +# If the pattern ends with ".*" it matches any class in the package. +# If the pattern ends with "*", it matches any class with the pattern as a prefix. +# If the pattern is equal to the class name, it matches. +# Otherwise, the status is UNDECIDED. +# +#jdk.serialFilter=pattern;pattern diff --git a/test/java/io/Serializable/serialFilter/CheckInputOrderTest.java b/test/java/io/Serializable/serialFilter/CheckInputOrderTest.java new file mode 100644 index 000000000..802ccb3e0 --- /dev/null +++ b/test/java/io/Serializable/serialFilter/CheckInputOrderTest.java @@ -0,0 +1,94 @@ +/* + * 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. + * + * 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 java.io.ByteArrayInputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.security.Security; + +import sun.misc.ObjectInputFilter; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; + +/* @test + * @build CheckInputOrderTest SerialFilterTest + * @run testng/othervm CheckInputOrderTest + * + * @summary Test that when both global filter and specific filter are set, + * global filter will not affect specific filter. + */ + +public class CheckInputOrderTest implements Serializable { + private static final long serialVersionUID = 12345678901L; + + @DataProvider(name="Patterns") + Object[][] patterns() { + return new Object[][] { + new Object[] { SerialFilterTest.genTestObject("maxarray=1", true), "java.**;java.lang.*;java.lang.Long;maxarray=0", false }, + new Object[] { SerialFilterTest.genTestObject("maxarray=1", true), "java.**;java.lang.*;java.lang.Long", true }, + new Object[] { Long.MAX_VALUE, "java.**;java.lang.*;java.lang.Long;maxdepth=0", false }, + new Object[] { Long.MAX_VALUE, "java.**;java.lang.*;java.lang.Long;maxbytes=0", false }, + new Object[] { Long.MAX_VALUE, "java.**;java.lang.*;java.lang.Long;maxrefs=0", false }, + + new Object[] { Long.MAX_VALUE, "java.**;java.lang.*;java.lang.Long", true }, + + new Object[] { Long.MAX_VALUE, "!java.**;java.lang.*;java.lang.Long", false }, + new Object[] { Long.MAX_VALUE, "java.**;!java.lang.*;java.lang.Long", true }, + + new Object[] { Long.MAX_VALUE, "!java.lang.*;java.**;java.lang.Long", false }, + new Object[] { Long.MAX_VALUE, "java.lang.*;!java.**;java.lang.Long", true }, + + new Object[] { Long.MAX_VALUE, "!java.lang.Long;java.**;java.lang.*", false }, + new Object[] { Long.MAX_VALUE, "java.lang.Long;java.**;!java.lang.*", true }, + + new Object[] { Long.MAX_VALUE, "java.lang.Long;!java.**;java.lang.*", false }, + new Object[] { Long.MAX_VALUE, "java.lang.Long;java.lang.Number;!java.**;java.lang.*", true }, + }; + } + + /** + * Test: + * "global filter reject" + "specific ObjectInputStream filter is empty" => should reject + * "global filter reject" + "specific ObjectInputStream filter allow" => should allow + */ + @Test(dataProvider="Patterns") + public void testRejectedInGlobal(Object toDeserialized, String pattern, boolean allowed) throws Exception { + byte[] bytes = SerialFilterTest.writeObjects(toDeserialized); + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern); + + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setObjectInputFilter(ois, filter); + Object o = ois.readObject(); + assertTrue(allowed, "filter should have thrown an exception"); + } catch (InvalidClassException ice) { + assertFalse(allowed, "filter should have thrown an exception"); + } + } +} diff --git a/test/java/io/Serializable/serialFilter/FilterWithSecurityManagerTest.java b/test/java/io/Serializable/serialFilter/FilterWithSecurityManagerTest.java new file mode 100644 index 000000000..7c03f91de --- /dev/null +++ b/test/java/io/Serializable/serialFilter/FilterWithSecurityManagerTest.java @@ -0,0 +1,101 @@ +/* + * 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. + * + * 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 java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; +import java.security.AccessControlException; + +import sun.misc.ObjectInputFilter; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/* @test + * @build FilterWithSecurityManagerTest SerialFilterTest + * @run testng/othervm FilterWithSecurityManagerTest + * @run testng/othervm/policy=security.policy.without.globalFilter + * -Djava.security.manager=default FilterWithSecurityManagerTest + * @run testng/othervm/policy=security.policy + * -Djava.security.manager=default + * -Djdk.serialFilter=java.lang.Integer FilterWithSecurityManagerTest + * + * @summary Test that setting specific filter is checked by security manager, + * setting process-wide filter is checked by security manager. + */ + +@Test +public class FilterWithSecurityManagerTest { + + byte[] bytes; + boolean setSecurityManager; + ObjectInputFilter filter; + + @BeforeClass + public void setup() throws Exception { + setSecurityManager = System.getSecurityManager() != null; + Object toDeserialized = Long.MAX_VALUE; + bytes = SerialFilterTest.writeObjects(toDeserialized); + filter = ObjectInputFilter.Config.createFilter("java.lang.Long"); + } + + /** + * Test that setting process-wide filter is checked by security manager. + */ + @Test + public void testGlobalFilter() throws Exception { + if (ObjectInputFilter.Config.getSerialFilter() == null) { + return; + } + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setSerialFilter(filter); + assertFalse(setSecurityManager, + "When SecurityManager exists, without " + + "java.security.SerializablePermission(serialFilter) Exception should be thrown"); + Object o = ois.readObject(); + } catch (AccessControlException ex) { + assertTrue(setSecurityManager); + assertTrue(ex.getMessage().contains("java.io.SerializablePermission")); + assertTrue(ex.getMessage().contains("serialFilter")); + } + } + + /** + * Test that setting specific filter is checked by security manager. + */ + @Test(dependsOnMethods = { "testGlobalFilter" }) + public void testSpecificFilter() throws Exception { + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setObjectInputFilter(ois, filter); + Object o = ois.readObject(); + } catch (AccessControlException ex) { + assertTrue(setSecurityManager); + assertTrue(ex.getMessage().contains("java.io.SerializablePermission")); + assertTrue(ex.getMessage().contains("serialFilter")); + } + } +} diff --git a/test/java/io/Serializable/serialFilter/GlobalFilterTest.java b/test/java/io/Serializable/serialFilter/GlobalFilterTest.java new file mode 100644 index 000000000..20503d19e --- /dev/null +++ b/test/java/io/Serializable/serialFilter/GlobalFilterTest.java @@ -0,0 +1,219 @@ +/* + * 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. + * + * 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 static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertSame; +import static org.testng.Assert.assertTrue; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; + +import java.io.SerializablePermission; +import java.security.Security; +import java.util.Objects; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +import sun.misc.ObjectInputFilter; + +/* @test + * @build GlobalFilterTest SerialFilterTest + * @run testng/othervm GlobalFilterTest + * @run testng/othervm -Djdk.serialFilter=java.** GlobalFilterTest + * @run testng/othervm/policy=security.policy GlobalFilterTest + * @run testng/othervm/policy=security.policy + * -Djava.security.properties=${test.src}/java.security-extra1 + * -Djava.security.debug=properties GlobalFilterTest + * + * @summary Test Global Filters + */ +@Test +public class GlobalFilterTest { + + /** + * DataProvider of patterns and objects derived from the configured process-wide filter. + * @return Array of arrays of pattern, object, allowed boolean, and API factory + */ + @DataProvider(name="globalPatternElements") + Object[][] globalPatternElements() { + String globalFilter = + System.getProperty("jdk.serialFilter", + Security.getProperty("jdk.serialFilter")); + if (globalFilter == null) { + return new Object[0][]; + } + + String[] patterns = globalFilter.split(";"); + Object[][] objects = new Object[patterns.length][]; + + for (int i = 0; i < patterns.length; i++) { + Object o; + boolean allowed; + String pattern = patterns[i].trim(); + if (pattern.contains("=")) { + allowed = false; + o = SerialFilterTest.genTestObject(pattern, false); + } else { + allowed = !pattern.startsWith("!"); + o = (allowed) + ? SerialFilterTest.genTestObject(pattern, true) + : SerialFilterTest.genTestObject(pattern.substring(1), false); + + Assert.assertNotNull(o, "fail generation failed"); + } + objects[i] = new Object[3]; + objects[i][0] = pattern; + objects[i][1] = allowed; + objects[i][2] = o; + } + return objects; + } + + /** + * Test that the process-wide filter is set when the properties are set + * and has the toString matching the configured pattern. + */ + @Test() + static void globalFilter() { + String pattern = + System.getProperty("jdk.serialFilter", + Security.getProperty("jdk.serialFilter")); + ObjectInputFilter filter = ObjectInputFilter.Config.getSerialFilter(); + System.out.printf("global pattern: %s, filter: %s%n", pattern, filter); + Assert.assertEquals(pattern, Objects.toString(filter, null), + "process-wide filter pattern does not match"); + } + + /** + * If the Global filter is already set, it should always refuse to be + * set again. + * If there is a security manager, setting the serialFilter should fail + * without the appropriate permission. + * If there is no security manager then setting it should work. + */ + @Test() + static void setGlobalFilter() { + SecurityManager sm = System.getSecurityManager(); + ObjectInputFilter filter = new SerialFilterTest.Validator(); + ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter(); + if (global != null) { + // once set, can never be re-set + try { + ObjectInputFilter.Config.setSerialFilter(filter); + Assert.fail("set only once process-wide filter"); + } catch (IllegalStateException ise) { + if (sm != null) { + Assert.fail("wrong exception when security manager is set", ise); + } + } catch (SecurityException se) { + if (sm == null) { + Assert.fail("wrong exception when security manager is not set", se); + } + } + } else { + if (sm == null) { + // no security manager + try { + ObjectInputFilter.Config.setSerialFilter(filter); + // Note once set, it can not be reset; so other tests + System.out.printf("Global Filter set to Validator%n"); + } catch (SecurityException se) { + Assert.fail("setGlobalFilter should not get SecurityException", se); + } + try { + // Try to set it again, expecting it to throw + ObjectInputFilter.Config.setSerialFilter(filter); + Assert.fail("set only once process-wide filter"); + } catch (IllegalStateException ise) { + // Normal case + } + } else { + // Security manager + SecurityException expectSE = null; + try { + sm.checkPermission(new SerializablePermission("serialFilter")); + } catch (SecurityException se1) { + expectSE = se1; + } + SecurityException actualSE = null; + try { + ObjectInputFilter.Config.setSerialFilter(filter); + } catch (SecurityException se2) { + actualSE = se2; + } + if (expectSE == null | actualSE == null) { + Assert.assertEquals(expectSE, actualSE, "SecurityException"); + } else { + Assert.assertEquals(expectSE.getClass(), actualSE.getClass(), + "SecurityException class"); + } + } + } + } + + /** + * For each pattern in the process-wide filter test a generated object + * against the default process-wide filter. + * + * @param pattern a pattern extracted from the configured global pattern + */ + @Test(dataProvider = "globalPatternElements") + static void globalFilterElements(String pattern, boolean allowed,Object obj) { + testGlobalPattern(pattern, obj, allowed); + } + + /** + * Serialize and deserialize an object using the default process-wide filter + * and check allowed or reject. + * + * @param pattern the pattern + * @param object the test object + * @param allowed the expected result from ObjectInputStream (exception or not) + */ + static void testGlobalPattern(String pattern, Object object, boolean allowed) { + try { +// System.out.printf("global %s pattern: %s, obj: %s%n", (allowed ? "allowed" : "not allowed"), pattern, object); + byte[] bytes = SerialFilterTest.writeObjects(object); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + Object o = ois.readObject(); + } catch (EOFException eof) { + // normal completion + } catch (ClassNotFoundException cnf) { + Assert.fail("Deserializing", cnf); + } + Assert.assertTrue(allowed, "filter should have thrown an exception"); + } catch (IllegalArgumentException iae) { + Assert.fail("bad format pattern", iae); + } catch (InvalidClassException ice) { + Assert.assertFalse(allowed, "filter should not have thrown an exception: " + ice); + } catch (IOException ioe) { + Assert.fail("Unexpected IOException", ioe); + } + } +} diff --git a/test/java/io/Serializable/serialFilter/MixedFiltersTest.java b/test/java/io/Serializable/serialFilter/MixedFiltersTest.java new file mode 100644 index 000000000..f5aeb010c --- /dev/null +++ b/test/java/io/Serializable/serialFilter/MixedFiltersTest.java @@ -0,0 +1,138 @@ +/* + * 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. + * + * 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 java.io.ByteArrayInputStream; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.security.Security; + +import sun.misc.ObjectInputFilter; + +import org.testng.annotations.BeforeClass; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +/* @test + * @build MixedFiltersTest SerialFilterTest + * @run testng/othervm -Djdk.serialFilter=!java.**;!java.lang.Long;maxdepth=5;maxarray=5;maxbytes=90;maxrefs=5 MixedFiltersTest + * @run testng/othervm -Djdk.serialFilter=java.**;java.lang.Long;maxdepth=1000;maxarray=1000;maxbytes=1000;maxrefs=1000 MixedFiltersTest + * + * @summary Test that when both global filter and specific filter are set, + * global filter will not affect specific filter. + */ + +public class MixedFiltersTest implements Serializable { + + private static final long serialVersionUID = 1234567890L; + + + boolean globalRejected; + + @BeforeClass + public void setup() { + String pattern = System.getProperty("jdk.serialFilter", + Security.getProperty("jdk.serialFilter")); + globalRejected = pattern.startsWith("!"); + } + + @DataProvider(name="RejectedInGlobal") + Object[][] rejectedInGlobal() { + if (!globalRejected) { + return new Object[0][]; + } + return new Object[][] { + new Object[] { Long.MAX_VALUE, "java.**" }, + new Object[] { Long.MAX_VALUE, "java.lang.Long" }, + new Object[] { SerialFilterTest.genTestObject("java.lang.**", true), "java.lang.**" }, + new Object[] { SerialFilterTest.genTestObject("maxdepth=10", true), "maxdepth=100" }, + new Object[] { SerialFilterTest.genTestObject("maxarray=10", true), "maxarray=100" }, + new Object[] { SerialFilterTest.genTestObject("maxbytes=100", true), "maxbytes=1000" }, + new Object[] { SerialFilterTest.genTestObject("maxrefs=10", true), "maxrefs=100" }, + }; + } + + /** + * Test: + * "global filter reject" + "specific ObjectInputStream filter is empty" => should reject + * "global filter reject" + "specific ObjectInputStream filter allow" => should allow + */ + @Test(dataProvider="RejectedInGlobal") + public void testRejectedInGlobal(Object toDeserialized, String pattern) throws Exception { + byte[] bytes = SerialFilterTest.writeObjects(toDeserialized); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + Object o = ois.readObject(); + fail("filter should have thrown an exception"); + } catch (InvalidClassException expected) { } + + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setObjectInputFilter(ois, filter); + Object o = ois.readObject(); + } + } + + @DataProvider(name="AllowedInGlobal") + Object[][] allowedInGlobal() { + if (globalRejected) { + return new Object[0][]; + } + + return new Object[][] { + new Object[] { Long.MAX_VALUE, "!java.**" }, + new Object[] { Long.MAX_VALUE, "!java.lang.Long" }, + new Object[] { SerialFilterTest.genTestObject("java.lang.**", true), "!java.lang.**" }, + new Object[] { SerialFilterTest.genTestObject("maxdepth=10", true), "maxdepth=5" }, + new Object[] { SerialFilterTest.genTestObject("maxarray=10", true), "maxarray=5" }, + new Object[] { SerialFilterTest.genTestObject("maxbytes=100", true), "maxbytes=5" }, + new Object[] { SerialFilterTest.genTestObject("maxrefs=10", true), "maxrefs=5" }, + }; + } + + /** + * Test: + * "global filter allow" + "specific ObjectInputStream filter is empty" => should allow + * "global filter allow" + "specific ObjectInputStream filter reject" => should reject + */ + @Test(dataProvider="AllowedInGlobal") + public void testAllowedInGlobal(Object toDeserialized, String pattern) throws Exception { + byte[] bytes = SerialFilterTest.writeObjects(toDeserialized); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + Object o = ois.readObject(); + } + + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern); + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setObjectInputFilter(ois, filter); + Object o = ois.readObject(); + assertTrue(false, "filter should have thrown an exception"); + } catch (InvalidClassException expected) { } + } +} diff --git a/test/java/io/Serializable/serialFilter/SerialFilterTest.java b/test/java/io/Serializable/serialFilter/SerialFilterTest.java new file mode 100644 index 000000000..0ab3246a6 --- /dev/null +++ b/test/java/io/Serializable/serialFilter/SerialFilterTest.java @@ -0,0 +1,743 @@ +/* + * 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. + * + * 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 java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Set; +import java.util.concurrent.atomic.LongAdder; + +import sun.misc.ObjectInputFilter; + +import javax.lang.model.SourceVersion; + +import org.testng.Assert; +import org.testng.annotations.Test; +import org.testng.annotations.DataProvider; + +/* @test + * @build SerialFilterTest + * @run testng/othervm SerialFilterTest + * + * @summary Test ObjectInputFilters + */ +@Test +public class SerialFilterTest implements Serializable { + + private static final long serialVersionUID = -6999613679881262446L; + + /** + * Enable three arg lambda. + * @param The pattern + * @param The test object + * @param Boolean for if the filter should allow or reject + */ + interface TriConsumer< T, U, V> { + void accept(T t, U u, V v); + } + + /** + * Misc object to use that should always be accepted. + */ + private static final Object otherObject = Integer.valueOf(0); + + /** + * DataProvider for the individual patterns to test. + * Expand the patterns into cases for each of the Std and Compatibility APIs. + * @return an array of arrays of the parameters including factories + */ + @DataProvider(name="Patterns") + static Object[][] patterns() { + Object[][] patterns = new Object[][]{ + {"java.util.Hashtable"}, + {"java.util.Hash*"}, + {"javax.lang.model.*"}, + {"javax.lang.**"}, + {"*"}, + {"maxarray=47"}, + {"maxdepth=5"}, + {"maxrefs=10"}, + {"maxbytes=100"}, + {"maxbytes=72"}, + {"maxbytes=+1024"}, + }; + return patterns; + } + + @DataProvider(name="InvalidPatterns") + static Object[][] invalidPatterns() { + return new Object [][] { + {"maxrefs=-1"}, + {"maxdepth=-1"}, + {"maxbytes=-1"}, + {"maxarray=-1"}, + {"xyz=0"}, + {"xyz=-1"}, + {"maxrefs=0xabc"}, + {"maxrefs=abc"}, + {"maxrefs="}, + {"maxrefs=+"}, + {".*"}, + {".**"}, + {"!"}, + {"/java.util.Hashtable"}, + {"java.base/"}, + {"/"}, + }; + } + + @DataProvider(name="Limits") + static Object[][] limits() { + // The numbers are arbitrary > 1 + return new Object[][]{ + {"maxrefs", 10}, + {"maxdepth", 5}, + {"maxbytes", 100}, + {"maxarray", 16}, + }; + } + + /** + * DataProvider of individual objects. Used to check the information + * available to the filter. + * @return Arrays of parameters with objects + */ + @DataProvider(name="Objects") + static Object[][] objects() { + byte[] byteArray = new byte[0]; + Object[] objArray = new Object[7]; + objArray[objArray.length - 1] = objArray; + + Class serClass = null; + String className = "java.util.concurrent.atomic.LongAdder$SerializationProxy"; + try { + serClass = Class.forName(className); + } catch (Exception e) { + Assert.fail("missing class: " + className, e); + } + + Class[] interfaces = {Runnable.class}; + Runnable proxy = (Runnable) Proxy.newProxyInstance(null, + interfaces, (p, m, args) -> p); + + Runnable runnable = (Runnable & Serializable) SerialFilterTest::noop; + Object[][] objects = { + { null, 0, -1, 0, 0, 0, + new HashSet<>()}, // no callback, no values + { objArray, 3, 7, 8, 2, 55, + new HashSet<>(Arrays.asList(objArray.getClass()))}, + { Object[].class, 1, -1, 1, 1, 40, + new HashSet<>(Arrays.asList(Object[].class))}, + { new SerialFilterTest(), 1, -1, 1, 1, 37, + new HashSet<>(Arrays.asList(SerialFilterTest.class))}, + { new LongAdder(), 2, -1, 1, 1, 93, + new HashSet<>(Arrays.asList(LongAdder.class, serClass))}, + { new byte[14], 2, 14, 1, 1, 27, + new HashSet<>(Arrays.asList(byteArray.getClass()))}, + { runnable, 13, 0, 10, 2, 514, + new HashSet<>(Arrays.asList(java.lang.invoke.SerializedLambda.class, + SerialFilterTest.class, + objArray.getClass()))}, + { deepHashSet(10), 48, -1, 49, 11, 619, + new HashSet<>(Arrays.asList(HashSet.class))}, + { proxy.getClass(), 3, -1, 1, 1, 114, + new HashSet<>(Arrays.asList(Runnable.class, + java.lang.reflect.Proxy.class))}, + }; + return objects; + } + + @DataProvider(name="Arrays") + static Object[][] arrays() { + return new Object[][]{ + {new Object[16], 16}, + {new boolean[16], 16}, + {new byte[16], 16}, + {new char[16], 16}, + {new int[16], 16}, + {new long[16], 16}, + {new short[16], 16}, + {new float[16], 16}, + {new double[16], 16}, + }; + } + + + /** + * Test each object and verify the classes identified by the filter, + * the count of calls to the filter, the max array size, max refs, max depth, + * max bytes. + * This test ignores/is not dependent on the global filter settings. + * + * @param object a Serializable object + * @param count the expected count of calls to the filter + * @param maxArray the maximum array size + * @param maxRefs the maximum references + * @param maxDepth the maximum depth + * @param maxBytes the maximum stream size + * @param classes the expected (unique) classes + * @throws IOException + */ + @Test(dataProvider="Objects") + public static void t1(Object object, + long count, long maxArray, long maxRefs, long maxDepth, long maxBytes, + Set> classes) throws IOException { + byte[] bytes = writeObjects(object); + Validator validator = new Validator(); + validate(bytes, validator); + System.out.printf("v: %s%n", validator); + Assert.assertEquals(validator.count, count, "callback count wrong"); + Assert.assertEquals(validator.classes, classes, "classes mismatch"); + Assert.assertEquals(validator.maxArray, maxArray, "maxArray mismatch"); + Assert.assertEquals(validator.maxRefs, maxRefs, "maxRefs wrong"); + Assert.assertEquals(validator.maxDepth, maxDepth, "depth wrong"); + Assert.assertEquals(validator.maxBytes, maxBytes, "maxBytes wrong"); + } + + /** + * Test each pattern with an appropriate object. + * A filter is created from the pattern and used to serialize and + * deserialize a generated object with both the positive and negative case. + * This test ignores/is not dependent on the global filter settings. + * + * @param pattern a pattern + */ + @Test(dataProvider="Patterns") + static void testPatterns(String pattern) { + evalPattern(pattern, (p, o, neg) -> testPatterns(p, o, neg)); + } + + /** + * Test that the filter on a OIS can be set only on a fresh OIS, + * before deserializing any objects. + * This test is agnostic the global filter being set or not. + */ + @Test + static void nonResettableFilter() { + Validator validator1 = new Validator(); + Validator validator2 = new Validator(); + + try { + byte[] bytes = writeObjects("text1"); // an object + + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + // Check the initial filter is the global filter; may be null + ObjectInputFilter global = ObjectInputFilter.Config.getSerialFilter(); + ObjectInputFilter initial = ObjectInputFilter.Config.getObjectInputFilter(ois); + Assert.assertEquals(global, initial, "initial filter should be the global filter"); + + // Check if it can be set to null + ObjectInputFilter.Config.setObjectInputFilter(ois, null); + ObjectInputFilter filter = ObjectInputFilter.Config.getObjectInputFilter(ois); + Assert.assertNull(filter, "set to null should be null"); + + ObjectInputFilter.Config.setObjectInputFilter(ois, validator1); + Object o = ois.readObject(); + try { + ObjectInputFilter.Config.setObjectInputFilter(ois, validator2); + Assert.fail("Should not be able to set filter twice"); + } catch (IllegalStateException ise) { + // success, the exception was expected + } + } catch (EOFException eof) { + Assert.fail("Should not reach end-of-file", eof); + } catch (ClassNotFoundException cnf) { + Assert.fail("Deserializing", cnf); + } + } catch (IOException ex) { + Assert.fail("Unexpected IOException", ex); + } + } + + /** + * Test that if an Objects readReadResolve method returns an array + * that the callback to the filter includes the proper array length. + * @throws IOException if an error occurs + */ + @Test(dataProvider="Arrays") + static void testReadResolveToArray(Object array, int length) throws IOException { + ReadResolveToArray object = new ReadResolveToArray(array, length); + byte[] bytes = writeObjects(object); + Object o = validate(bytes, object); // the object is its own filter + Assert.assertEquals(o.getClass(), array.getClass(), "Filter not called with the array"); + } + + /** + * Test repeated limits use the last value. + * Construct a filter with the limit and the limit repeated -1. + * Invoke the filter with the limit to make sure it is rejected. + * Invoke the filter with the limit -1 to make sure it is accepted. + * @param name the name of the limit to test + * @param value a test value + */ + @Test(dataProvider="Limits") + static void testLimits(String name, int value) { + Class arrayClass = new int[0].getClass(); + String pattern = String.format("%s=%d;%s=%d", name, value, name, value - 1); + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern); + Assert.assertEquals( + filter.checkInput(new FilterValues(arrayClass, value, value, value, value)), + ObjectInputFilter.Status.REJECTED, + "last limit value not used: " + filter); + Assert.assertEquals( + filter.checkInput(new FilterValues(arrayClass, value-1, value-1, value-1, value-1)), + ObjectInputFilter.Status.UNDECIDED, + "last limit value not used: " + filter); + } + + /** + * Test that returning null from a filter causes deserialization to fail. + */ + @Test(expectedExceptions=InvalidClassException.class) + static void testNullStatus() throws IOException { + byte[] bytes = writeObjects(0); // an Integer + try { + Object o = validate(bytes, new ObjectInputFilter() { + public ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo f) { + return null; + } + }); + } catch (InvalidClassException ice) { + System.out.printf("Success exception: %s%n", ice); + throw ice; + } + } + + /** + * Verify that malformed patterns throw IAE. + * @param pattern pattern from the data source + */ + @Test(dataProvider="InvalidPatterns", expectedExceptions=IllegalArgumentException.class) + static void testInvalidPatterns(String pattern) { + try { + ObjectInputFilter.Config.createFilter(pattern); + } catch (IllegalArgumentException iae) { + System.out.printf(" success exception: %s%n", iae); + throw iae; + } + } + + /** + * Test that Config.create returns null if the argument does not contain any patterns or limits. + */ + @Test() + static void testEmptyPattern() { + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(""); + Assert.assertNull(filter, "empty pattern did not return null"); + + filter = ObjectInputFilter.Config.createFilter(";;;;"); + Assert.assertNull(filter, "pattern with only delimiters did not return null"); + } + + /** + * Read objects from the serialized stream, validated with the filter. + * + * @param bytes a byte array to read objects from + * @param filter the ObjectInputFilter + * @return the object deserialized if any + * @throws IOException can be thrown + */ + static Object validate(byte[] bytes, + ObjectInputFilter filter) throws IOException { + try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes); + ObjectInputStream ois = new ObjectInputStream(bais)) { + ObjectInputFilter.Config.setObjectInputFilter(ois, filter); + + Object o = ois.readObject(); + return o; + } catch (EOFException eof) { + // normal completion + } catch (ClassNotFoundException cnf) { + Assert.fail("Deserializing", cnf); + } + return null; + } + + /** + * Write objects and return a byte array with the bytes. + * + * @param objects zero or more objects to serialize + * @return the byte array of the serialized objects + * @throws IOException if an exception occurs + */ + static byte[] writeObjects(Object... objects) throws IOException { + byte[] bytes; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream oos = new ObjectOutputStream(baos)) { + for (Object o : objects) { + oos.writeObject(o); + } + bytes = baos.toByteArray(); + } + return bytes; + } + + /** + * A filter that accumulates information about the checkInput callbacks + * that can be checked after readObject completes. + */ + static class Validator implements ObjectInputFilter { + long count; // Count of calls to checkInput + HashSet> classes = new HashSet<>(); + long maxArray = -1; + long maxRefs; + long maxDepth; + long maxBytes; + + Validator() { + } + + @Override + public ObjectInputFilter.Status checkInput(FilterInfo filter) { + count++; + if (filter.serialClass() != null) { + if (filter.serialClass().getName().contains("$$Lambda$")) { + // TBD: proper identification of serialized Lambdas? + // Fold the serialized Lambda into the SerializedLambda type + classes.add(SerializedLambda.class); + } else if (Proxy.isProxyClass(filter.serialClass())) { + classes.add(Proxy.class); + } else { + classes.add(filter.serialClass()); + } + + } + this.maxArray = Math.max(this.maxArray, filter.arrayLength()); + this.maxRefs = Math.max(this.maxRefs, filter.references()); + this.maxDepth = Math.max(this.maxDepth, filter.depth()); + this.maxBytes = Math.max(this.maxBytes, filter.streamBytes()); + return ObjectInputFilter.Status.UNDECIDED; + } + + public String toString(){ + return "count: " + count + + ", classes: " + classes.toString() + + ", maxArray: " + maxArray + + ", maxRefs: " + maxRefs + + ", maxDepth: " + maxDepth + + ", maxBytes: " + maxBytes; + } + } + + + /** + * Create a filter from a pattern and API factory, then serialize and + * deserialize an object and check allowed or reject. + * + * @param pattern the pattern + * @param object the test object + * @param allowed the expected result from ObjectInputStream (exception or not) + */ + static void testPatterns(String pattern, Object object, boolean allowed) { + try { + byte[] bytes = SerialFilterTest.writeObjects(object); + ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(pattern); + validate(bytes, filter); + Assert.assertTrue(allowed, "filter should have thrown an exception"); + } catch (IllegalArgumentException iae) { + Assert.fail("bad format pattern", iae); + } catch (InvalidClassException ice) { + Assert.assertFalse(allowed, "filter should not have thrown an exception: " + ice); + } catch (IOException ioe) { + Assert.fail("Unexpected IOException", ioe); + } + } + + /** + * For a filter pattern, generate and apply a test object to the action. + * @param pattern a pattern + * @param action an action to perform on positive and negative cases + */ + static void evalPattern(String pattern, TriConsumer action) { + Object o = genTestObject(pattern, true); + Assert.assertNotNull(o, "success generation failed"); + action.accept(pattern, o, true); + + // Test the negative pattern + o = genTestObject(pattern, false); + Assert.assertNotNull(o, "fail generation failed"); + String negPattern = pattern.contains("=") ? pattern : "!" + pattern; + action.accept(negPattern, o, false); + } + + /** + * Generate a test object based on the pattern. + * Handles each of the forms of the pattern, wildcards, + * class name, various limit forms. + * @param pattern a pattern + * @param allowed a boolean indicating to generate the allowed or disallowed case + * @return an object or {@code null} to indicate no suitable object could be generated + */ + static Object genTestObject(String pattern, boolean allowed) { + if (pattern.contains("=")) { + return genTestLimit(pattern, allowed); + } else if (pattern.endsWith("*")) { + return genTestObjectWildcard(pattern, allowed); + } else { + // class + try { + Class clazz = Class.forName(pattern); + Constructor cons = clazz.getConstructor(); + return cons.newInstance(); + } catch (ClassNotFoundException ex) { + Assert.fail("no such class available: " + pattern); + } catch (InvocationTargetException + | NoSuchMethodException + | InstantiationException + | IllegalAccessException ex1) { + Assert.fail("newInstance: " + ex1); + } + } + return null; + } + + /** + * Generate an object to be used with the various wildcard pattern forms. + * Explicitly supports only specific package wildcards with specific objects. + * @param pattern a wildcard pattern ending in "*" + * @param allowed a boolean indicating to generate the allowed or disallowed case + * @return an object within or outside the wildcard + */ + static Object genTestObjectWildcard(String pattern, boolean allowed) { + if (pattern.endsWith(".**")) { + // package hierarchy wildcard + if (pattern.startsWith("javax.lang.")) { + return SourceVersion.RELEASE_5; + } + if (pattern.startsWith("java.")) { + return 4; + } + if (pattern.startsWith("javax.")) { + return SourceVersion.RELEASE_6; + } + return otherObject; + } else if (pattern.endsWith(".*")) { + // package wildcard + if (pattern.startsWith("javax.lang.model")) { + return SourceVersion.RELEASE_6; + } + } else { + // class wildcard + if (pattern.equals("*")) { + return otherObject; // any object will do + } + if (pattern.startsWith("java.util.Hash")) { + return new Hashtable(); + } + } + Assert.fail("Object could not be generated for pattern: " + + pattern + + ", allowed: " + allowed); + return null; + } + + /** + * Generate a limit test object for the pattern. + * For positive cases, the object exactly hits the limit. + * For negative cases, the object is 1 greater than the limit + * @param pattern the pattern, containing "=" and a maxXXX keyword + * @param allowed a boolean indicating to generate the allowed or disallowed case + * @return a sitable object + */ + static Object genTestLimit(String pattern, boolean allowed) { + int ndx = pattern.indexOf('='); + Assert.assertNotEquals(ndx, -1, "missing value in limit"); + long value = Long.parseUnsignedLong(pattern.substring(ndx+1)); + if (pattern.startsWith("maxdepth=")) { + // Return an object with the requested depth (or 1 greater) + long depth = allowed ? value : value + 1; + Object[] array = new Object[1]; + for (int i = 1; i < depth; i++) { + Object[] n = new Object[1]; + n[0] = array; + array = n; + } + return array; + } else if (pattern.startsWith("maxbytes=")) { + // Return a byte array that when written to OOS creates + // a stream of exactly the size requested. + return genMaxBytesObject(allowed, value); + } else if (pattern.startsWith("maxrefs=")) { + Object[] array = new Object[allowed ? (int)value - 1 : (int)value]; + for (int i = 0; i < array.length; i++) { + array[i] = otherObject; + } + return array; + } else if (pattern.startsWith("maxarray=")) { + return allowed ? new int[(int)value] : new int[(int)value+1]; + } + Assert.fail("Object could not be generated for pattern: " + + pattern + + ", allowed: " + allowed); + return null; + } + + /** + * Generate an an object that will be serialized to some number of bytes. + * Or 1 greater if allowed is false. + * It returns a two element Object array holding a byte array sized + * to achieve the desired total size. + * @param allowed true if the stream should be allowed at that size, + * false if the stream should be larger + * @param maxBytes the number of bytes desired in the stream; + * should not be less than 72 (due to protocol overhead). + * @return a object that will be serialized to the length requested + */ + private static Object genMaxBytesObject(boolean allowed, long maxBytes) { + Object[] holder = new Object[2]; + long desiredSize = allowed ? maxBytes : maxBytes + 1; + long actualSize = desiredSize; + long byteSize = desiredSize - 72; // estimate needed array size + do { + byteSize += (desiredSize - actualSize); + byte[] a = new byte[(int)byteSize]; + holder[0] = a; + holder[1] = a; + try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream os = new ObjectOutputStream(baos)) { + os.writeObject(holder); + os.flush(); + actualSize = baos.size(); + } catch (IOException ie) { + Assert.fail("exception generating stream", ie); + } + } while (actualSize != desiredSize); + return holder; + } + + /** + * Returns a HashSet of a requested depth. + * @param depth the depth + * @return a HashSet of HashSets... + */ + static HashSet deepHashSet(int depth) { + HashSet hashSet = new HashSet<>(); + HashSet s1 = hashSet; + HashSet s2 = new HashSet<>(); + for (int i = 0; i < depth; i++ ) { + HashSet t1 = new HashSet<>(); + HashSet t2 = new HashSet<>(); + // make t1 not equal to t2 + t1.add("by Jimminy"); + s1.add(t1); + s1.add(t2); + s2.add(t1); + s2.add(t2); + s1 = t1; + s2 = t2; + } + return hashSet; + } + + /** + * Simple method to use with Serialized Lambda. + */ + private static void noop() {} + + + /** + * Class that returns an array from readResolve and also implements + * the ObjectInputFilter to check that it has the expected length. + */ + static class ReadResolveToArray implements Serializable, ObjectInputFilter { + private static final long serialVersionUID = 123456789L; + + private final Object array; + private final int length; + + ReadResolveToArray(Object array, int length) { + this.array = array; + this.length = length; + } + + Object readResolve() { + return array; + } + + @Override + public ObjectInputFilter.Status checkInput(FilterInfo filter) { + if (ReadResolveToArray.class.isAssignableFrom(filter.serialClass())) { + return ObjectInputFilter.Status.ALLOWED; + } + if (filter.serialClass() != array.getClass() || + (filter.arrayLength() >= 0 && filter.arrayLength() != length)) { + return ObjectInputFilter.Status.REJECTED; + } + return ObjectInputFilter.Status.UNDECIDED; + } + + } + + /** + * Hold a snapshot of values to be passed to an ObjectInputFilter. + */ + static class FilterValues implements ObjectInputFilter.FilterInfo { + private final Class clazz; + private final long arrayLength; + private final long depth; + private final long references; + private final long streamBytes; + + public FilterValues(Class clazz, long arrayLength, long depth, long references, long streamBytes) { + this.clazz = clazz; + this.arrayLength = arrayLength; + this.depth = depth; + this.references = references; + this.streamBytes = streamBytes; + } + + @Override + public Class serialClass() { + return clazz; + } + + public long arrayLength() { + return arrayLength; + } + + public long depth() { + return depth; + } + + public long references() { + return references; + } + + public long streamBytes() { + return streamBytes; + } + } +} diff --git a/test/java/io/Serializable/serialFilter/java.security-extra1 b/test/java/io/Serializable/serialFilter/java.security-extra1 new file mode 100644 index 000000000..7a52040c6 --- /dev/null +++ b/test/java/io/Serializable/serialFilter/java.security-extra1 @@ -0,0 +1,4 @@ +# Serialization Input Process-wide Filter +# See conf/security/java.security for pattern synatx +# +jdk.serialFilter=java.**;javax.**;maxarray=34;maxdepth=7 diff --git a/test/java/io/Serializable/serialFilter/security.policy b/test/java/io/Serializable/serialFilter/security.policy new file mode 100644 index 000000000..f986e255e --- /dev/null +++ b/test/java/io/Serializable/serialFilter/security.policy @@ -0,0 +1,17 @@ +// Individual Permissions to for GlobalFilterTest +grant { + // Specific permission under test + permission java.security.SerializablePermission "serialFilter"; + // Permissions needed to run the test + permission java.util.PropertyPermission "*", "read"; + permission java.io.FilePermission "<>", "read,write,delete"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.security.SecurityPermission "*"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; +}; + +// Standard extensions get all permissions by default +grant codeBase "file:${{java.ext.dirs}}/*" { + permission java.security.AllPermission; +}; diff --git a/test/java/io/Serializable/serialFilter/security.policy.without.globalFilter b/test/java/io/Serializable/serialFilter/security.policy.without.globalFilter new file mode 100644 index 000000000..2f80b82ff --- /dev/null +++ b/test/java/io/Serializable/serialFilter/security.policy.without.globalFilter @@ -0,0 +1,15 @@ +// Individual Permissions for FilterWithSecurityManagerTest +grant { + // Permissions needed to run the test + permission java.util.PropertyPermission "*", "read"; + permission java.io.FilePermission "<>", "read,write,delete"; + permission java.lang.reflect.ReflectPermission "suppressAccessChecks"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; + permission java.lang.RuntimePermission "accessClassInPackage.sun.misc"; +}; + +// Standard extensions get all permissions by default +grant codeBase "file:${{java.ext.dirs}}/*" { + permission java.security.AllPermission; +}; + -- GitLab