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.ArrayList;
27  import java.util.List;
28  import java.util.regex.Pattern;
29  import java.util.stream.Stream;
30  
31  /**
32   * An {@link ObjectInputStream} that's restricted to deserialize
33   * a limited set of classes.
34   *
35   * <p>
36   * Various accept/reject methods allow for specifying which classes
37   * can be deserialized.
38   * </p>
39   *
40   * <p>
41   * Design inspired by <a
42   * href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM
43   * DeveloperWorks Article</a>.
44   * </p>
45   */
46  public class ValidatingObjectInputStream extends ObjectInputStream {
47      private final List<ClassNameMatcher> acceptMatchers = new ArrayList<>();
48      private final List<ClassNameMatcher> rejectMatchers = new ArrayList<>();
49  
50      /**
51       * Constructs an object to deserialize the specified input stream.
52       * At least one accept method needs to be called to specify which
53       * classes can be deserialized, as by default no classes are
54       * accepted.
55       *
56       * @param input an input stream
57       * @throws IOException if an I/O error occurs while reading stream header
58       */
59      public ValidatingObjectInputStream(final InputStream input) throws IOException {
60          super(input);
61      }
62  
63      /**
64       * Accept the specified classes for deserialization, unless they
65       * are otherwise rejected.
66       *
67       * @param classes Classes to accept
68       * @return this object
69       */
70      public ValidatingObjectInputStream accept(final Class<?>... classes) {
71          Stream.of(classes).map(c -> new FullClassNameMatcher(c.getName())).forEach(acceptMatchers::add);
72          return this;
73      }
74  
75      /**
76       * Accept class names where the supplied ClassNameMatcher matches for
77       * deserialization, unless they are otherwise rejected.
78       *
79       * @param m the matcher to use
80       * @return this object
81       */
82      public ValidatingObjectInputStream accept(final ClassNameMatcher m) {
83          acceptMatchers.add(m);
84          return this;
85      }
86  
87      /**
88       * Accept class names that match the supplied pattern for
89       * deserialization, unless they are otherwise rejected.
90       *
91       * @param pattern standard Java regexp
92       * @return this object
93       */
94      public ValidatingObjectInputStream accept(final Pattern pattern) {
95          acceptMatchers.add(new RegexpClassNameMatcher(pattern));
96          return this;
97      }
98  
99      /**
100      * Accept the wildcard specified classes for deserialization,
101      * unless they are otherwise rejected.
102      *
103      * @param patterns Wildcard file name patterns as defined by
104      *                  {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch}
105      * @return this object
106      */
107     public ValidatingObjectInputStream accept(final String... patterns) {
108         Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(acceptMatchers::add);
109         return this;
110     }
111 
112     /**
113      * Checks that the class name conforms to requirements.
114      *
115      * @param name The class name
116      * @throws InvalidClassException when a non-accepted class is encountered
117      */
118     private void checkClassName(final String name) throws InvalidClassException {
119         // Reject has precedence over accept
120         for (final ClassNameMatcher m : rejectMatchers) {
121             if (m.matches(name)) {
122                 invalidClassNameFound(name);
123             }
124         }
125 
126         boolean ok = false;
127         for (final ClassNameMatcher m : acceptMatchers) {
128             if (m.matches(name)) {
129                 ok = true;
130                 break;
131             }
132         }
133         if (!ok) {
134             invalidClassNameFound(name);
135         }
136     }
137 
138     /**
139      * Called to throw {@link InvalidClassException} if an invalid
140      * class name is found during deserialization. Can be overridden, for example
141      * to log those class names.
142      *
143      * @param className name of the invalid class
144      * @throws InvalidClassException if the specified class is not allowed
145      */
146     protected void invalidClassNameFound(final String className) throws InvalidClassException {
147         throw new InvalidClassException("Class name not accepted: " + className);
148     }
149 
150     /**
151      * Reject the specified classes for deserialization, even if they
152      * are otherwise accepted.
153      *
154      * @param classes Classes to reject
155      * @return this object
156      */
157     public ValidatingObjectInputStream reject(final Class<?>... classes) {
158         Stream.of(classes).map(c -> new FullClassNameMatcher(c.getName())).forEach(rejectMatchers::add);
159         return this;
160     }
161 
162     /**
163      * Reject class names where the supplied ClassNameMatcher matches for
164      * deserialization, even if they are otherwise accepted.
165      *
166      * @param m the matcher to use
167      * @return this object
168      */
169     public ValidatingObjectInputStream reject(final ClassNameMatcher m) {
170         rejectMatchers.add(m);
171         return this;
172     }
173 
174     /**
175      * Reject class names that match the supplied pattern for
176      * deserialization, even if they are otherwise accepted.
177      *
178      * @param pattern standard Java regexp
179      * @return this object
180      */
181     public ValidatingObjectInputStream reject(final Pattern pattern) {
182         rejectMatchers.add(new RegexpClassNameMatcher(pattern));
183         return this;
184     }
185 
186     /**
187      * Reject the wildcard specified classes for deserialization,
188      * even if they are otherwise accepted.
189      *
190      * @param patterns Wildcard file name patterns as defined by
191      *                  {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch}
192      * @return this object
193      */
194     public ValidatingObjectInputStream reject(final String... patterns) {
195         Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(rejectMatchers::add);
196         return this;
197     }
198 
199     @Override
200     protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
201         checkClassName(osc.getName());
202         return super.resolveClass(osc);
203     }
204 }