diff --git a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java index 495d02677b6..c4ce0e1d65b 100644 --- a/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java +++ b/src/main/java/org/apache/commons/io/serialization/ValidatingObjectInputStream.java @@ -23,6 +23,7 @@ import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; +import java.lang.annotation.Annotation; import java.util.regex.Pattern; import org.apache.commons.io.build.AbstractStreamBuilder; @@ -145,6 +146,8 @@ public static class Builder extends AbstractStreamBuilder + * The reject list takes precedence over the accept list. + *

* * @param classes Classes to accept. * @return this object. @@ -169,6 +175,9 @@ public Builder accept(final Class... classes) { /** * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected. + *

+ * The reject list takes precedence over the accept list. + *

* * @param matcher a class name matcher to accept objects. * @return {@code this} instance. @@ -181,6 +190,9 @@ public Builder accept(final ClassNameMatcher matcher) { /** * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected. + *

+ * The reject list takes precedence over the accept list. + *

* * @param pattern a Pattern for compiled regular expression. * @return {@code this} instance. @@ -193,6 +205,9 @@ public Builder accept(final Pattern pattern) { /** * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected. + *

+ * The reject list takes precedence over the accept list. + *

* * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} @@ -207,6 +222,9 @@ public Builder accept(final String... patterns) { /** * Builds a new {@link ValidatingObjectInputStream}. *

+ * The reject list takes precedence over the accept list. + *

+ *

* You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception. *

*

@@ -242,6 +260,9 @@ public ObjectStreamClassPredicate getPredicate() { /** * Rejects the specified classes for deserialization, even if they are otherwise accepted. + *

+ * The reject list takes precedence over the accept list. + *

* * @param classes Classes to reject. * @return {@code this} instance. @@ -254,6 +275,9 @@ public Builder reject(final Class... classes) { /** * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted. + *

+ * The reject list takes precedence over the accept list. + *

* * @param matcher the matcher to use. * @return {@code this} instance. @@ -266,6 +290,9 @@ public Builder reject(final ClassNameMatcher matcher) { /** * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted. + *

+ * The reject list takes precedence over the accept list. + *

* * @param pattern standard Java regexp. * @return {@code this} instance. @@ -278,6 +305,9 @@ public Builder reject(final Pattern pattern) { /** * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted. + *

+ * The reject list takes precedence over the accept list. + *

* * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) * FilenameUtils.wildcardMatch} @@ -291,6 +321,9 @@ public Builder reject(final String... patterns) { /** * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate. + *

+ * The reject list takes precedence over the accept list. + *

* * @param predicate the predicate. * @return {@code this} instance. @@ -301,10 +334,39 @@ public Builder setPredicate(final ObjectStreamClassPredicate predicate) { return this; } + /** + * If true, checks that all interfaces and annotations for a given type are not rejected and accepted. + *

+ * For compatibility with previous versions, this is false by default. + *

+ *

+ * Checks: + *

+ *
    + *
  1. The type name.
  2. + *
  3. The interfaces implemented by the class, recursively.
  4. + *
  5. The annotation on this type, recursively.
  6. + *
+ *

+ * The reject list takes precedence over the accept list. + *

+ * + * @param strict If true, checks that all interfaces and annotations for a given type are not rejected and accepted. + * @return {@code this} instance. + * @since 2.23.0 + */ + public Builder setStrict(final boolean strict) { + this.strict = strict; + return this; + } + } /** * Constructs a new {@link Builder}. + *

+ * The reject list takes precedence over the accept list. + *

* * @return a new {@link Builder}. * @since 2.18.0 @@ -314,36 +376,38 @@ public static Builder builder() { } private final ObjectStreamClassPredicate predicate; - - @SuppressWarnings("resource") // caller closes/ - private ValidatingObjectInputStream(final Builder builder) throws IOException { - this(builder.getInputStream(), builder.predicate); - } + private final boolean strict; /** * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be * deserialized, as by default no classes are accepted. + *

+ * The reject list takes precedence over the accept list. + *

* - * @param input an input stream. - * @throws IOException if an I/O error occurs while reading stream header. - * @deprecated Use {@link #builder()}. + * @param builder The builder. */ - @Deprecated - public ValidatingObjectInputStream(final InputStream input) throws IOException { - this(input, new ObjectStreamClassPredicate()); + @SuppressWarnings("resource") // caller closes + private ValidatingObjectInputStream(final Builder builder) throws IOException { + super(builder.getInputStream()); + this.predicate = builder.predicate; + this.strict = builder.strict; } /** * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be * deserialized, as by default no classes are accepted. + *

+ * The reject list takes precedence over the accept list. + *

* - * @param input an input stream. - * @param predicate how to accept and reject classes. + * @param input an input stream. * @throws IOException if an I/O error occurs while reading stream header. + * @deprecated Use {@link #builder()}. */ - private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException { - super(input); - this.predicate = predicate; + @Deprecated + public ValidatingObjectInputStream(final InputStream input) throws IOException { + this(builder().setInputStream(input).setPredicate(new ObjectStreamClassPredicate())); } /** @@ -404,15 +468,38 @@ public ValidatingObjectInputStream accept(final String... patterns) { } /** - * Checks that the class name conforms to requirements. + * Checks that the given type can be legally resolved as configured. + *

+ * Checks: + *

    + *
  1. The type name.
  2. + *
  3. The interfaces implemented by the class, recursively.
  4. + *
  5. The annotation on this type, recursively.
  6. + *
+ * + * @param result + * @throws InvalidClassException + */ + private void check(final Class result) throws InvalidClassException { + checkTypeName(result.getName()); + for (final Class iface : result.getInterfaces()) { + check(iface); + } + for (final Annotation annotation : result.getAnnotations()) { + check(annotation.annotationType()); + } + } + + /** + * Checks that the type name conforms to requirements. *

* The reject list takes precedence over the accept list. *

* - * @param name The class name to test. + * @param name The type name to test. * @throws InvalidClassException Thrown when a rejected or non-accepted class is found. */ - private void checkClassName(final String name) throws InvalidClassException { + private void checkTypeName(final String name) throws InvalidClassException { if (!predicate.test(name)) { invalidClassNameFound(name); } @@ -509,8 +596,14 @@ public ValidatingObjectInputStream reject(final String... patterns) { */ @Override protected Class resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException { - checkClassName(osc.getName()); - return super.resolveClass(osc); + checkTypeName(osc.getName()); + // resolveClass() calls Class.forName(String, boolean, ClassLoader) with initialize set to false. + // The result Class is therefore not initialized. + final Class result = super.resolveClass(osc); + if (strict) { + check(result); + } + return result; } /** @@ -522,7 +615,7 @@ protected Class resolveClass(final ObjectStreamClass osc) throws IOException, @Override protected Class resolveProxyClass(final String[] interfaces) throws IOException, ClassNotFoundException { for (final String interfaceName : interfaces) { - checkClassName(interfaceName); + checkTypeName(interfaceName); } return super.resolveProxyClass(interfaces); } diff --git a/src/test/java/org/apache/commons/io/serialization/ValidatingObjectInputStream2Test.java b/src/test/java/org/apache/commons/io/serialization/ValidatingObjectInputStream2Test.java new file mode 100644 index 00000000000..b81ec744900 --- /dev/null +++ b/src/test/java/org/apache/commons/io/serialization/ValidatingObjectInputStream2Test.java @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.apache.commons.io.serialization; + +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.InvalidClassException; +import java.io.Serializable; + +import org.apache.commons.lang3.SerializationUtils; +import org.junit.jupiter.api.Test; + +/** + * Tests {@link ValidatingObjectInputStream}. + */ +class ValidatingObjectInputStream2Test { + + abstract static class AbtractFoo implements IFoo { + + private static final long serialVersionUID = 1L; + + } + + public static class FixtureObject implements IFoo { + + private static final long serialVersionUID = 1L; + + @Override + public void foo() { + // empty + } + } + + static class FooImpl extends AbtractFoo { + + private static final long serialVersionUID = 1L; + + @Override + public void foo() { + // empty + } + + } + + @FunctionalInterface + public interface IFoo extends Serializable { + + void foo(); + } + + @Test + void testAcceptAbstractClass() throws IOException, ClassNotFoundException { + final FooImpl object = new FooImpl(); + final byte[] serialized = SerializationUtils.serialize(object); + final Class ifaceClass = IFoo.class; + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .accept(ifaceClass) + .accept(Serializable.class) + .accept(AbtractFoo.class) + .accept(FooImpl.class) + .get()) { + // @formatter:on + assertInstanceOf(ifaceClass, vois.readObject()); + } + } + + @Test + void testAcceptAll() throws IOException, ClassNotFoundException { + final FixtureObject object = new FixtureObject(); + final byte[] serialized = SerializationUtils.serialize(object); + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .accept("*") + .get()) { + // @formatter:on + assertInstanceOf(IFoo.class, vois.readObject()); + } + } + + @Test + void testAcceptInterface() throws IOException, ClassNotFoundException { + final FixtureObject object = new FixtureObject(); + final byte[] serialized = SerializationUtils.serialize(object); + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .accept(IFoo.class) + .get()) { + // @formatter:on + // not a feature + assertThrows(InvalidClassException.class, vois::readObject); + } + } + + @Test + void testRejectAnnotation() throws IOException, ClassNotFoundException { + final FixtureObject object = new FixtureObject(); + final byte[] serialized = SerializationUtils.serialize(object); + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .reject(FunctionalInterface.class) + .accept("*") + .setStrict(true) + .get()) { + // @formatter:on + assertThrows(InvalidClassException.class, vois::readObject); + } + } + + @Test + void testRejectInterface() throws IOException, ClassNotFoundException { + final FixtureObject object = new FixtureObject(); + final byte[] serialized = SerializationUtils.serialize(object); + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .reject(IFoo.class) + .accept("*") + .setStrict(true) + .get()) { + // @formatter:on + assertThrows(InvalidClassException.class, vois::readObject); + } + } + + @Test + void testRejectSuperClass() throws IOException, ClassNotFoundException { + final FooImpl object = new FooImpl(); + final byte[] serialized = SerializationUtils.serialize(object); + // @formatter:off + try (ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() + .setByteArray(serialized) + .setStrict(true) + .setPredicate(new ObjectStreamClassPredicate() { + @Override + public boolean test(String name) { + // System.out.println(name); + return super.test(name); + } + }) + // after setting the debug predicate above. + .reject(AbtractFoo.class) + .accept("*") + .get()) { + // @formatter:on + assertThrows(InvalidClassException.class, vois::readObject); + } + } +}