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   *   http://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  
30  /**
31   * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes.
32   *
33   * <p>
34   * Various accept/reject methods allow for specifying which classes can be deserialized.
35   * </p>
36   * <h2>Reading safely</h2>
37   * <p>
38   * Here is the only way to safely read a HashMap of String keys and Integer values:
39   * </p>
40   *
41   * <pre>{@code
42   * // Defining Object fixture
43   * final HashMap<String, Integer> map1 = new HashMap<>();
44   * map1.put("1", 1);
45   * // Writing serialized fixture
46   * final byte[] byteArray;
47   * try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
48   *         final ObjectOutputStream oos = new ObjectOutputStream(baos)) {
49   *     oos.writeObject(map1);
50   *     oos.flush();
51   *     byteArray = baos.toByteArray();
52   * }
53   * // Reading
54   * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
55   *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
56   *             .accept(HashMap.class, Number.class, Integer.class)
57   *             .setInputStream(bais)
58   *             .get()) {
59   *     // String.class is automatically accepted
60   *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
61   *     assertEquals(map1, map2);
62   * }
63   * // Reusing a configuration
64   * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate()
65   *     .accept(HashMap.class, Number.class, Integer.class);
66   * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray);
67   *         ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder()
68   *             .setPredicate(predicate)
69   *             .setInputStream(bais)
70   *             .get()) {
71   *     // String.class is automatically accepted
72   *     final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject();
73   *     assertEquals(map1, map2);
74   * }
75   * }</pre>
76   * <p>
77   * Design inspired by a <a href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>.
78   * </p>
79   *
80   * @since 2.5
81   */
82  public class ValidatingObjectInputStream extends ObjectInputStream {
83  
84      // @formatter:off
85      /**
86       * Builds a new {@link ValidatingObjectInputStream}.
87       *
88       * <h2>Using NIO</h2>
89       * <pre>{@code
90       * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
91       *   .setPath(Paths.get("MyFile.ser"))
92       *   .get();}
93       * </pre>
94       * <h2>Using IO</h2>
95       * <pre>{@code
96       * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder()
97       *   .setFile(new File("MyFile.ser"))
98       *   .get();}
99       * </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 }