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 }