    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 *   https://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;
    029import org.apache.commons.io.input.BoundedInputStream;
    030
    031/**
    032 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
    033 *
    034 * <p>
    035 * Various accept/reject methods allow for specifying which classes can be deserialized.
    036 * </p>
    037 * <h2>Deserlizing safely</h2>
    038 * <p>
    039 * Here is the only way to safely read a HashMap of String keys and Integer values:
    040 * </p>
    041 *
    042 * <pre>{@code
    043 * // Defining Object fixture
    044 * final HashMap<String, Integer> map1 = new HashMap<>();
    045 * map1.put("1", 1);
    046 * // Writing serialized fixture
    047 * final byte[] byteArray;
    048 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
    049 *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
    050 *     oos.writeObject(map1);
    051 *     oos.flush();
    052 *     byteArray = baos.toByteArray();
    053 * }
    054 * // Deserializing
    055 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    056 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    057 *             .accept(HashMap.class, Number.class, Integer.class)
    058 *             .setInputStream(bais)
    059 *             .get()) {
    060 *     // String.class is automatically accepted
    061 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
    062 *     assertEquals(map1, map2);
    063 * }
    064 * // Reusing a configuration
    065 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
    066 *     .accept(HashMap.class, Number.class, Integer.class);
    067 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    068 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    069 *             .setPredicate(predicate)
    070 *             .setInputStream(bais)
    071 *             .get()) {
    072 *     // String.class is automatically accepted
    073 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
    074 *     assertEquals(map1, map2);
    075 * }
    076 * }</pre>
    077 * <p>
    078 * This design was inspired by a <a href="https://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
    079 * </p>
    080 * <h2>Deserlizing with a size boundary</h2>
    081 * <p>
    082 * You can further guard your application againt untrusted input by limiting how much data to process using a {@link BoundedInputStream}.
    083 * For example:
    084 * </p>
    085 * <pre>{@code
    086 * // Deserializing with a size limit successfully
    087 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    088 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    089 *             .accept(HashMap.class, Number.class, Integer.class)
    090 *             .setInputStream(BoundedInputStream.builder()
    091 *                 .setMaxCount(10_000)
    092 *                 .setOnMaxCount((max, count) -> {
    093 *                     throw new IllegalArgumentException("Input exceeds limit.");
    094 *                 })
    095 *                 .setInputStream(bais)
    096 *                 .get())
    097 *             .get()) {
    098 *     // String.class is automatically accepted
    099 *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
    100 *     assertEquals(map1, map2);
    101 * }
    102 * // Deserializing with a size limit reaching the limit
    103 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
    104 *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
    105 *             .accept(HashMap.class, Number.class, Integer.class)
    106 *             .setInputStream(BoundedInputStream.builder()
    107 *                 .setMaxCount(10)
    108 *                 .setOnMaxCount((max, count) -> {
    109 *                     throw new IllegalArgumentException("Input exceeds limit.");
    110 *                 })
    111 *                 .setInputStream(bais)
    112 *                 .get())
    113 *             .get()) {
    114 *     // String.class is automatically accepted
    115 *     final IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> vois.readObject());
    116 *     assertEquals("Input exceeds limit.", e.getMessage());
    117 * }
    118 * }</pre>
    119 * @since 2.5
    120 */
    121public class ValidatingObjectInputStream extends ObjectInputStream {
    122
    123    // @formatter:off
    124    /**
    125     * Builds a new {@link ValidatingObjectInputStream}.
    126     *
    127     * <h2>Using NIO</h2>
    128     * <pre>{@code
    129     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
    130     *   .setPath(Paths.get("MyFile.ser"))
    131     *   .get();}
    132     * </pre>
    133     * <h2>Using IO</h2>
    134     * <pre>{@code
    135     * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
    136     *   .setFile(new File("MyFile.ser"))
    137     *   .get();}
    138     * </pre>
    139     *
    140     * @see #get()
    141     * @since 2.18.0
    142     */
    143    // @formatter:on
    144    public static class Builder extends AbstractStreamBuilder<ValidatingObjectInputStream, Builder> {
    145
    146        private ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate();
    147
    148        /**
    149         * Constructs a new builder of {@link ValidatingObjectInputStream}.
    150         *
    151         * @deprecated Use {@link #builder()}.
    152         */
    153        @Deprecated
    154        public Builder() {
    155            // empty
    156        }
    157
    158        /**
    159         * Accepts the specified classes for deserialization, unless they are otherwise rejected.
    160         *
    161         * @param classes Classes to accept.
    162         * @return this object.
    163         * @since 2.18.0
    164         */
    165        public Builder accept(final Class<?>... classes) {
    166            predicate.accept(classes);
    167            return this;
    168        }
    169
    170        /**
    171         * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
    172         *
    173         * @param matcher a class name matcher to <em>accept</em> objects.
    174         * @return {@code this} instance.
    175         * @since 2.18.0
    176         */
    177        public Builder accept(final ClassNameMatcher matcher) {
    178            predicate.accept(matcher);
    179            return this;
    180        }
    181
    182        /**
    183         * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
    184         *
    185         * @param pattern a Pattern for compiled regular expression.
    186         * @return {@code this} instance.
    187         * @since 2.18.0
    188         */
    189        public Builder accept(final Pattern pattern) {
    190            predicate.accept(pattern);
    191            return this;
    192        }
    193
    194        /**
    195         * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
    196         *
    197         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    198         *                 FilenameUtils.wildcardMatch}
    199         * @return {@code this} instance.
    200         * @since 2.18.0
    201         */
    202        public Builder accept(final String... patterns) {
    203            predicate.accept(patterns);
    204            return this;
    205        }
    206
    207        /**
    208         * Builds a new {@link ValidatingObjectInputStream}.
    209         * <p>
    210         * You must set an aspect that supports {@link #getInputStream()} on this builder, otherwise, this method throws an exception.
    211         * </p>
    212         * <p>
    213         * This builder uses the following aspects:
    214         * </p>
    215         * <ul>
    216         * <li>{@link #getInputStream()} gets the target aspect.</li>
    217         * <li>predicate</li>
    218         * <li>charsetDecoder</li>
    219         * <li>writeImmediately</li>
    220         * </ul>
    221         *
    222         * @return a new instance.
    223         * @throws UnsupportedOperationException if the origin cannot provide a {@link InputStream}.
    224         * @throws IOException                   if an I/O error occurs converting to an {@link InputStream} using {@link #getInputStream()}.
    225         * @see #getWriter()
    226         * @see #getUnchecked()
    227         */
    228        @Override
    229        public ValidatingObjectInputStream get() throws IOException {
    230            return new ValidatingObjectInputStream(this);
    231        }
    232
    233        /**
    234         * Gets the predicate.
    235         *
    236         * @return the predicate.
    237         * @since 2.18.0
    238         */
    239        public ObjectStreamClassPredicate getPredicate() {
    240            return predicate;
    241        }
    242
    243        /**
    244         * Rejects the specified classes for deserialization, even if they are otherwise accepted.
    245         *
    246         * @param classes Classes to reject.
    247         * @return {@code this} instance.
    248         * @since 2.18.0
    249         */
    250        public Builder reject(final Class<?>... classes) {
    251            predicate.reject(classes);
    252            return this;
    253        }
    254
    255        /**
    256         * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
    257         *
    258         * @param matcher the matcher to use.
    259         * @return {@code this} instance.
    260         * @since 2.18.0
    261         */
    262        public Builder reject(final ClassNameMatcher matcher) {
    263            predicate.reject(matcher);
    264            return this;
    265        }
    266
    267        /**
    268         * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
    269         *
    270         * @param pattern standard Java regexp.
    271         * @return {@code this} instance.
    272         * @since 2.18.0
    273         */
    274        public Builder reject(final Pattern pattern) {
    275            predicate.reject(pattern);
    276            return this;
    277        }
    278
    279        /**
    280         * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
    281         *
    282         * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    283         *                 FilenameUtils.wildcardMatch}
    284         * @return {@code this} instance.
    285         * @since 2.18.0
    286         */
    287        public Builder reject(final String... patterns) {
    288            predicate.reject(patterns);
    289            return this;
    290        }
    291
    292        /**
    293         * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate.
    294         *
    295         * @param predicate the predicate.
    296         * @return {@code this} instance.
    297         * @since 2.18.0
    298         */
    299        public Builder setPredicate(final ObjectStreamClassPredicate predicate) {
    300            this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate();
    301            return this;
    302        }
    303
    304    }
    305
    306    /**
    307     * Constructs a new {@link Builder}.
    308     *
    309     * @return a new {@link Builder}.
    310     * @since 2.18.0
    311     */
    312    public static Builder builder() {
    313        return new Builder();
    314    }
    315
    316    private final ObjectStreamClassPredicate predicate;
    317
    318    @SuppressWarnings("resource") // caller closes/
    319    private ValidatingObjectInputStream(final Builder builder) throws IOException {
    320        this(builder.getInputStream(), builder.predicate);
    321    }
    322
    323    /**
    324     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
    325     * deserialized, as by default no classes are accepted.
    326     *
    327     * @param input an input stream.
    328     * @throws IOException if an I/O error occurs while reading stream header.
    329     * @deprecated Use {@link #builder()}.
    330     */
    331    @Deprecated
    332    public ValidatingObjectInputStream(final InputStream input) throws IOException {
    333        this(input, new ObjectStreamClassPredicate());
    334    }
    335
    336    /**
    337     * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
    338     * deserialized, as by default no classes are accepted.
    339     *
    340     * @param input     an input stream.
    341     * @param predicate how to accept and reject classes.
    342     * @throws IOException if an I/O error occurs while reading stream header.
    343     */
    344    private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
    345        super(input);
    346        this.predicate = predicate;
    347    }
    348
    349    /**
    350     * Accepts the specified classes for deserialization, unless they are otherwise rejected.
    351     * <p>
    352     * The reject list takes precedence over the accept list.
    353     * </p>
    354     *
    355     * @param classes Classes to accept.
    356     * @return {@code this} instance.
    357     */
    358    public ValidatingObjectInputStream accept(final Class<?>... classes) {
    359        predicate.accept(classes);
    360        return this;
    361    }
    362
    363    /**
    364     * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
    365     * <p>
    366     * The reject list takes precedence over the accept list.
    367     * </p>
    368     *
    369     * @param matcher a class name matcher to <em>accept</em> objects.
    370     * @return {@code this} instance.
    371     */
    372    public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
    373        predicate.accept(matcher);
    374        return this;
    375    }
    376
    377    /**
    378     * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
    379     * <p>
    380     * The reject list takes precedence over the accept list.
    381     * </p>
    382     *
    383     * @param pattern a Pattern for compiled regular expression.
    384     * @return {@code this} instance.
    385     */
    386    public ValidatingObjectInputStream accept(final Pattern pattern) {
    387        predicate.accept(pattern);
    388        return this;
    389    }
    390
    391    /**
    392     * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
    393     * <p>
    394     * The reject list takes precedence over the accept list.
    395     * </p>
    396     *
    397     * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    398     *                 FilenameUtils.wildcardMatch}.
    399     * @return {@code this} instance.
    400     */
    401    public ValidatingObjectInputStream accept(final String... patterns) {
    402        predicate.accept(patterns);
    403        return this;
    404    }
    405
    406    /**
    407     * Checks that the class name conforms to requirements.
    408     * <p>
    409     * The reject list takes precedence over the accept list.
    410     * </p>
    411     *
    412     * @param name The class name to test.
    413     * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
    414     */
    415    private void checkClassName(final String name) throws InvalidClassException {
    416        if (!predicate.test(name)) {
    417            invalidClassNameFound(name);
    418        }
    419    }
    420
    421    /**
    422     * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
    423     * names.
    424     *
    425     * @param className name of the invalid class.
    426     * @throws InvalidClassException Thrown with a message containing the class name.
    427     */
    428    protected void invalidClassNameFound(final String className) throws InvalidClassException {
    429        throw new InvalidClassException("Class name not accepted: " + className);
    430    }
    431
    432    /**
    433     * Delegates to {@link #readObject()} and casts to the generic {@code T}.
    434     *
    435     * @param <T> The return type.
    436     * @return Result from {@link #readObject()}.
    437     * @throws ClassNotFoundException Thrown by {@link #readObject()}.
    438     * @throws IOException            Thrown by {@link #readObject()}.
    439     * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
    440     * @since 2.18.0
    441     */
    442    @SuppressWarnings("unchecked")
    443    public <T> T readObjectCast() throws ClassNotFoundException, IOException {
    444        return (T) super.readObject();
    445    }
    446
    447    /**
    448     * Rejects the specified classes for deserialization, even if they are otherwise accepted.
    449     * <p>
    450     * The reject list takes precedence over the accept list.
    451     * </p>
    452     *
    453     * @param classes Classes to reject.
    454     * @return {@code this} instance.
    455     */
    456    public ValidatingObjectInputStream reject(final Class<?>... classes) {
    457        predicate.reject(classes);
    458        return this;
    459    }
    460
    461    /**
    462     * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
    463     * <p>
    464     * The reject list takes precedence over the accept list.
    465     * </p>
    466     *
    467     * @param matcher a class name matcher to <em>reject</em> objects.
    468     * @return {@code this} instance.
    469     */
    470    public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
    471        predicate.reject(matcher);
    472        return this;
    473    }
    474
    475    /**
    476     * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
    477     * <p>
    478     * The reject list takes precedence over the accept list.
    479     * </p>
    480     *
    481     * @param pattern a Pattern for compiled regular expression.
    482     * @return {@code this} instance.
    483     */
    484    public ValidatingObjectInputStream reject(final Pattern pattern) {
    485        predicate.reject(pattern);
    486        return this;
    487    }
    488
    489    /**
    490     * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
    491     * <p>
    492     * The reject list takes precedence over the accept list.
    493     * </p>
    494     *
    495     * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
    496     *                 FilenameUtils.wildcardMatch}
    497     * @return {@code this} instance.
    498     */
    499    public ValidatingObjectInputStream reject(final String... patterns) {
    500        predicate.reject(patterns);
    501        return this;
    502    }
    503
    504    /**
    505     * Checks that the given object's class name conforms to requirements and if so delegates to the superclass.
    506     * <p>
    507     * The reject list takes precedence over the accept list.
    508     * </p>
    509     */
    510    @Override
    511    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
    512        checkClassName(osc.getName());
    513        return super.resolveClass(osc);
    514    }
    515
    516    /**
    517     * Checks that the given names conform to requirements and if so delegates to the superclass.
    518     * <p>
    519     * The reject list takes precedence over the accept list.
    520     * </p>
    521     */
    522    @Override
    523    protected Class<?> resolveProxyClass(final String[] interfaces) throws IOException, ClassNotFoundException {
    524        for (final String interfaceName : interfaces) {
    525            checkClassName(interfaceName);
    526        }
    527        return super.resolveProxyClass(interfaces);
    528    }
    529}