View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   https://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.commons.io.serialization;
20  
21  import java.io.IOException;
22  import java.io.InputStream;
23  import java.io.InvalidClassException;
24  import java.io.ObjectInputStream;
25  import java.io.ObjectStreamClass;
26  import java.util.regex.Pattern;
27  
28  import org.apache.commons.io.build.AbstractStreamBuilder;
29  import org.apache.commons.io.input.BoundedInputStream;
30  
31  /**
32   * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
33   *
34   * <p>
35   * Various accept/reject methods allow for specifying which classes can be deserialized.
36   * </p>
37   * <h2>Deserlizing safely</h2>
38   * <p>
39   * Here is the only way to safely read a HashMap of String keys and Integer values:
40   * </p>
41   *
42   * <pre>{@code
43   * // Defining Object fixture
44   * final HashMap<String, Integer> map1 = new HashMap<>();
45   * map1.put("1", 1);
46   * // Writing serialized fixture
47   * final byte[] byteArray;
48   * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
49   *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
50   *     oos.writeObject(map1);
51   *     oos.flush();
52   *     byteArray = baos.toByteArray();
53   * }
54   * // Deserializing
55   * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
56   *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
57   *             .accept(HashMap.class, Number.class, Integer.class)
58   *             .setInputStream(bais)
59   *             .get()) {
60   *     // String.class is automatically accepted
61   *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
62   *     assertEquals(map1, map2);
63   * }
64   * // Reusing a configuration
65   * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
66   *     .accept(HashMap.class, Number.class, Integer.class);
67   * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
68   *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
69   *             .setPredicate(predicate)
70   *             .setInputStream(bais)
71   *             .get()) {
72   *     // String.class is automatically accepted
73   *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
74   *     assertEquals(map1, map2);
75   * }
76   * }</pre>
77   * <p>
78   * This design was inspired by a <a href="https://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
79   * </p>
80   * <h2>Deserlizing with a size boundary</h2>
81   * <p>
82   * You can further guard your application againt untrusted input by limiting how much data to process using a {@link BoundedInputStream}.
83   * For example:
84   * </p>
85   * <pre>{@code
86   * // Deserializing with a size limit successfully
87   * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
88   *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
89   *             .accept(HashMap.class, Number.class, Integer.class)
90   *             .setInputStream(BoundedInputStream.builder()
91   *                 .setMaxCount(10_000)
92   *                 .setOnMaxCount((max, count) -> {
93   *                     throw new IllegalArgumentException("Input exceeds limit.");
94   *                 })
95   *                 .setInputStream(bais)
96   *                 .get())
97   *             .get()) {
98   *     // String.class is automatically accepted
99   *     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  */
121 public 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 }