    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 *   http://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019package org.apache.commons.io.serialization;
    020
    021import java.io.IOException;
    022import java.io.InputStream;
    023import java.io.InvalidClassException;
    024import java.io.ObjectInputStream;
    025import java.io.ObjectStreamClass;
    026import java.util.regex.Pattern;
    027
    028import org.apache.commons.io.build.AbstractStreamBuilder;
    029
    030/**
    031 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
    032 *
    033 * <p>
    034 * Various accept/reject methods allow for specifying which classes can be deserialized.
    035 * </p>
    036 * <h2>Reading safely</h2>
    037 * <p>
    038 * Here is the only way to safely read a HashMap of String keys and Integer values:
    039 * </p>
    040 *
    041 * <pre>{@code
    042 * // Defining Object fixture
    043 * final HashMap<String, Integer> map1 = new HashMap<>();
    044 * map1.put("1", 1);
    045 * // Writing serialized fixture
    046 * final byte[] byteArray;
    047 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
    048 *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    049 *     oos.writeObject(map1);
    050 *     oos.flush();
    051 *     byteArray = baos.toByteArray();
    052 * }
    053 * // Reading
    054 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    055 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    056 *             .accept(HashMap.class, Number.class, Integer.class)
    057 *             .setInputStream(bais)
    058 *             .get()) {
    059 *     // String.class is automatically accepted
    060 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
    061 *     assertEquals(map1, map2);
    062 * }
    063 * // Reusing a configuration
    064 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
    065 *     .accept(HashMap.class, Number.class, Integer.class);
    066 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    067 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    068 *             .setPredicate(predicate)
    069 *             .setInputStream(bais)
    070 *             .get()) {
    071 *     // String.class is automatically accepted
    072 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
    073 *     assertEquals(map1, map2);
    074 * }
    075 * }</pre>
    076 * <p>
    077 * Design inspired by a <a href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
    078 * </p>
    079 *
    080 * @since 2.5
    081 */
    082public class ValidatingObjectInputStream extends ObjectInputStream {
    083
    084    // @formatter:off
    085    /**
    086     * Builds a new {@link ValidatingObjectInputStream}.
    087     *
    088     * <h2>Using NIO</h2>
    089     * <pre>{@code
    090     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
    091     *   .setPath(Paths.get("MyFile.ser"))
    092     *   .get();}
    093     * </pre>
    094     * <h2>Using IO</h2>
    095     * <pre>{@code
    096     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
    097     *   .setFile(new File("MyFile.ser"))
    098     *   .get();}
    099     * </pre>
    100     *
    101     * @see #get()
    102     * @since 2.18.0
    103     */
    104    // @formatter:on
    105    public static class Builder extends AbstractStreamBuilder<ValidatingObjectInputStream, Builder> {
    106
    107        private ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate();
    108
    109        /**
    110         * Constructs a new builder of {@link ValidatingObjectInputStream}.
    111         *
    112         * @deprecated Use {@link #builder()}.
    113         */
    114        @Deprecated
    115        public Builder() {
    116            // empty
    117        }
    118
    119        /**
    120         * Accepts the specified classes for deserialization, unless they are otherwise rejected.
    121         *
    122         * @param classes Classes to accept
    123         * @return this object
    124         * @since 2.18.0
    125         */
    126        public Builder accept(final Class<?>... classes) {
    127            predicate.accept(classes);
    128            return this;
    129        }
    130
    131        /**
    132         * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
    133         *
    134         * @param matcher a class name matcher to <em>accept</em> objects.
    135         * @return this instance.
    136         * @since 2.18.0
    137         */
    138        public Builder accept(final ClassNameMatcher matcher) {
    139            predicate.accept(matcher);
    140            return this;
    141        }
    142
    143        /**
    144         * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
    145         *
    146         * @param pattern a Pattern for compiled regular expression.
    147         * @return this instance.
    148         * @since 2.18.0
    149         */
    150        public Builder accept(final Pattern pattern) {
    151            predicate.accept(pattern);
    152            return this;
    153        }
    154
    155        /**
    156         * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
    157         *
    158         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    159         *                 FilenameUtils.wildcardMatch}
    160         * @return this instance.
    161         * @since 2.18.0
    162         */
    163        public Builder accept(final String... patterns) {
    164            predicate.accept(patterns);
    165            return this;
    166        }
    167
    168        /**
    169         * Builds a new {@link ValidatingObjectInputStream}.
    170         * <p>
    171         * You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
    172         * </p>
    173         * <p>
    174         * This builder uses the following aspects:
    175         * </p>
    176         * <ul>
    177         * <li>{@link #getInputStream()} gets the target aspect.</li>
    178         * <li>predicate</li>
    179         * <li>charsetDecoder</li>
    180         * <li>writeImmediately</li>
    181         * </ul>
    182         *
    183         * @return a new instance.
    184         * @throws UnsupportedOperationException if the origin cannot provide a {@link InputStream}.
    185         * @throws IOException                   if an I/O error occurs converting to an {@link InputStream} using {@link #getInputStream()}.
    186         * @see #getWriter()
    187         * @see #getUnchecked()
    188         */
    189        @Override
    190        public ValidatingObjectInputStream get() throws IOException {
    191            return new ValidatingObjectInputStream(getInputStream(), predicate);
    192        }
    193
    194        /**
    195         * Gets the predicate.
    196         *
    197         * @return the predicate.
    198         * @since 2.18.0
    199         */
    200        public ObjectStreamClassPredicate getPredicate() {
    201            return predicate;
    202        }
    203
    204        /**
    205         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
    206         *
    207         * @param classes Classes to reject
    208         * @return this instance.
    209         * @since 2.18.0
    210         */
    211        public Builder reject(final Class<?>... classes) {
    212            predicate.reject(classes);
    213            return this;
    214        }
    215
    216        /**
    217         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
    218         *
    219         * @param matcher the matcher to use
    220         * @return this instance.
    221         * @since 2.18.0
    222         */
    223        public Builder reject(final ClassNameMatcher matcher) {
    224            predicate.reject(matcher);
    225            return this;
    226        }
    227
    228        /**
    229         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
    230         *
    231         * @param pattern standard Java regexp
    232         * @return this instance.
    233         * @since 2.18.0
    234         */
    235        public Builder reject(final Pattern pattern) {
    236            predicate.reject(pattern);
    237            return this;
    238        }
    239
    240        /**
    241         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
    242         *
    243         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    244         *                 FilenameUtils.wildcardMatch}
    245         * @return this instance.
    246         * @since 2.18.0
    247         */
    248        public Builder reject(final String... patterns) {
    249            predicate.reject(patterns);
    250            return this;
    251        }
    252
    253        /**
    254         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
    255         *
    256         * @param predicate the predicate.
    257         * @return this instance.
    258         * @since 2.18.0
    259         */
    260        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
    261            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
    262            return this;
    263        }
    264
    265    }
    266
    267    /**
    268     * Constructs a new {@link Builder}.
    269     *
    270     * @return a new {@link Builder}.
    271     * @since 2.18.0
    272     */
    273    public static Builder builder() {
    274        return new Builder();
    275    }
    276
    277    private final ObjectStreamClassPredicate predicate;
    278
    279    /**
    280     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
    281     * deserialized, as by default no classes are accepted.
    282     *
    283     * @param input an input stream
    284     * @throws IOException if an I/O error occurs while reading stream header
    285     * @deprecated Use {@link #builder()}.
    286     */
    287    @Deprecated
    288    public ValidatingObjectInputStream(final InputStream input) throws IOException {
    289        this(input, new ObjectStreamClassPredicate());
    290    }
    291
    292    /**
    293     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
    294     * deserialized, as by default no classes are accepted.
    295     *
    296     * @param input     an input stream.
    297     * @param predicate how to accept and reject classes.
    298     * @throws IOException if an I/O error occurs while reading stream header.
    299     */
    300    private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
    301        super(input);
    302        this.predicate = predicate;
    303    }
    304
    305    /**
    306     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
    307     * <p>
    308     * The reject list takes precedence over the accept list.
    309     * </p>
    310     *
    311     * @param classes Classes to accept
    312     * @return this instance.
    313     */
    314    public ValidatingObjectInputStream accept(final Class<?>... classes) {
    315        predicate.accept(classes);
    316        return this;
    317    }
    318
    319    /**
    320     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
    321     * <p>
    322     * The reject list takes precedence over the accept list.
    323     * </p>
    324     *
    325     * @param matcher a class name matcher to <em>accept</em> objects.
    326     * @return this instance.
    327     */
    328    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
    329        predicate.accept(matcher);
    330        return this;
    331    }
    332
    333    /**
    334     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
    335     * <p>
    336     * The reject list takes precedence over the accept list.
    337     * </p>
    338     *
    339     * @param pattern a Pattern for compiled regular expression.
    340     * @return this instance.
    341     */
    342    public ValidatingObjectInputStream accept(final Pattern pattern) {
    343        predicate.accept(pattern);
    344        return this;
    345    }
    346
    347    /**
    348     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
    349     * <p>
    350     * The reject list takes precedence over the accept list.
    351     * </p>
    352     *
    353     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    354     *                 FilenameUtils.wildcardMatch}.
    355     * @return this instance.
    356     */
    357    public ValidatingObjectInputStream accept(final String... patterns) {
    358        predicate.accept(patterns);
    359        return this;
    360    }
    361
    362    /**
    363     * Checks that the class name conforms to requirements.
    364     * <p>
    365     * The reject list takes precedence over the accept list.
    366     * </p>
    367     *
    368     * @param name The class name to test.
    369     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
    370     */
    371    private void checkClassName(final String name) throws InvalidClassException {
    372        if (!predicate.test(name)) {
    373            invalidClassNameFound(name);
    374        }
    375    }
    376
    377    /**
    378     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
    379     * names.
    380     *
    381     * @param className name of the invalid class.
    382     * @throws InvalidClassException Thrown with a message containing the class name.
    383     */
    384    protected void invalidClassNameFound(final String className) throws InvalidClassException {
    385        throw new InvalidClassException("Class name not accepted: " + className);
    386    }
    387
    388    /**
    389     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
    390     *
    391     * @param <T> The return type.
    392     * @return Result from {@link #readObject()}.
    393     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
    394     * @throws IOException            Thrown by {@link #readObject()}.
    395     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
    396     * @since 2.18.0
    397     */
    398    @SuppressWarnings("unchecked")
    399    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
    400        return (T) super.readObject();
    401    }
    402
    403    /**
    404     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
    405     * <p>
    406     * The reject list takes precedence over the accept list.
    407     * </p>
    408     *
    409     * @param classes Classes to reject.
    410     * @return this instance.
    411     */
    412    public ValidatingObjectInputStream reject(final Class<?>... classes) {
    413        predicate.reject(classes);
    414        return this;
    415    }
    416
    417    /**
    418     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
    419     * <p>
    420     * The reject list takes precedence over the accept list.
    421     * </p>
    422     *
    423     * @param matcher a class name matcher to <em>reject</em> objects.
    424     * @return this instance.
    425     */
    426    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
    427        predicate.reject(matcher);
    428        return this;
    429    }
    430
    431    /**
    432     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
    433     * <p>
    434     * The reject list takes precedence over the accept list.
    435     * </p>
    436     *
    437     * @param pattern a Pattern for compiled regular expression.
    438     * @return this instance.
    439     */
    440    public ValidatingObjectInputStream reject(final Pattern pattern) {
    441        predicate.reject(pattern);
    442        return this;
    443    }
    444
    445    /**
    446     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
    447     * <p>
    448     * The reject list takes precedence over the accept list.
    449     * </p>
    450     *
    451     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    452     *                 FilenameUtils.wildcardMatch}
    453     * @return this instance.
    454     */
    455    public ValidatingObjectInputStream reject(final String... patterns) {
    456        predicate.reject(patterns);
    457        return this;
    458    }
    459
    460    @Override
    461    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
    462        checkClassName(osc.getName());
    463        return super.resolveClass(osc);
    464    }
    465}