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}