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 }