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  
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(this);
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     @SuppressWarnings("resource") // caller closes/
280     private ValidatingObjectInputStream(final Builder builder) throws IOException {
281         this(builder.getInputStream(), builder.predicate);
282     }
283 
284     /**
285      * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
286      * deserialized, as by default no classes are accepted.
287      *
288      * @param input an input stream
289      * @throws IOException if an I/O error occurs while reading stream header
290      * @deprecated Use {@link #builder()}.
291      */
292     @Deprecated
293     public ValidatingObjectInputStream(final InputStream input) throws IOException {
294         this(input, new ObjectStreamClassPredicate());
295     }
296 
297     /**
298      * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be
299      * deserialized, as by default no classes are accepted.
300      *
301      * @param input     an input stream.
302      * @param predicate how to accept and reject classes.
303      * @throws IOException if an I/O error occurs while reading stream header.
304      */
305     private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException {
306         super(input);
307         this.predicate = predicate;
308     }
309 
310     /**
311      * Accepts the specified classes for deserialization, unless they are otherwise rejected.
312      * <p>
313      * The reject list takes precedence over the accept list.
314      * </p>
315      *
316      * @param classes Classes to accept
317      * @return this instance.
318      */
319     public ValidatingObjectInputStream accept(final Class<?>... classes) {
320         predicate.accept(classes);
321         return this;
322     }
323 
324     /**
325      * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected.
326      * <p>
327      * The reject list takes precedence over the accept list.
328      * </p>
329      *
330      * @param matcher a class name matcher to <em>accept</em> objects.
331      * @return this instance.
332      */
333     public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) {
334         predicate.accept(matcher);
335         return this;
336     }
337 
338     /**
339      * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected.
340      * <p>
341      * The reject list takes precedence over the accept list.
342      * </p>
343      *
344      * @param pattern a Pattern for compiled regular expression.
345      * @return this instance.
346      */
347     public ValidatingObjectInputStream accept(final Pattern pattern) {
348         predicate.accept(pattern);
349         return this;
350     }
351 
352     /**
353      * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected.
354      * <p>
355      * The reject list takes precedence over the accept list.
356      * </p>
357      *
358      * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
359      *                 FilenameUtils.wildcardMatch}.
360      * @return this instance.
361      */
362     public ValidatingObjectInputStream accept(final String... patterns) {
363         predicate.accept(patterns);
364         return this;
365     }
366 
367     /**
368      * Checks that the class name conforms to requirements.
369      * <p>
370      * The reject list takes precedence over the accept list.
371      * </p>
372      *
373      * @param name The class name to test.
374      * @throws InvalidClassException Thrown when a rejected or non-accepted class is found.
375      */
376     private void checkClassName(final String name) throws InvalidClassException {
377         if (!predicate.test(name)) {
378             invalidClassNameFound(name);
379         }
380     }
381 
382     /**
383      * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class
384      * names.
385      *
386      * @param className name of the invalid class.
387      * @throws InvalidClassException Thrown with a message containing the class name.
388      */
389     protected void invalidClassNameFound(final String className) throws InvalidClassException {
390         throw new InvalidClassException("Class name not accepted: " + className);
391     }
392 
393     /**
394      * Delegates to {@link #readObject()} and casts to the generic {@code T}.
395      *
396      * @param <T> The return type.
397      * @return Result from {@link #readObject()}.
398      * @throws ClassNotFoundException Thrown by {@link #readObject()}.
399      * @throws IOException            Thrown by {@link #readObject()}.
400      * @throws ClassCastException     Thrown when {@link #readObject()} does not match {@code T}.
401      * @since 2.18.0
402      */
403     @SuppressWarnings("unchecked")
404     public <T> T readObjectCast() throws ClassNotFoundException, IOException {
405         return (T) super.readObject();
406     }
407 
408     /**
409      * Rejects the specified classes for deserialization, even if they are otherwise accepted.
410      * <p>
411      * The reject list takes precedence over the accept list.
412      * </p>
413      *
414      * @param classes Classes to reject.
415      * @return this instance.
416      */
417     public ValidatingObjectInputStream reject(final Class<?>... classes) {
418         predicate.reject(classes);
419         return this;
420     }
421 
422     /**
423      * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted.
424      * <p>
425      * The reject list takes precedence over the accept list.
426      * </p>
427      *
428      * @param matcher a class name matcher to <em>reject</em> objects.
429      * @return this instance.
430      */
431     public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) {
432         predicate.reject(matcher);
433         return this;
434     }
435 
436     /**
437      * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted.
438      * <p>
439      * The reject list takes precedence over the accept list.
440      * </p>
441      *
442      * @param pattern a Pattern for compiled regular expression.
443      * @return this instance.
444      */
445     public ValidatingObjectInputStream reject(final Pattern pattern) {
446         predicate.reject(pattern);
447         return this;
448     }
449 
450     /**
451      * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted.
452      * <p>
453      * The reject list takes precedence over the accept list.
454      * </p>
455      *
456      * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String)
457      *                 FilenameUtils.wildcardMatch}
458      * @return this instance.
459      */
460     public ValidatingObjectInputStream reject(final String... patterns) {
461         predicate.reject(patterns);
462         return this;
463     }
464 
465     @Override
466     protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
467         checkClassName(osc.getName());
468         return super.resolveClass(osc);
469     }
470 }