001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * https://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.commons.io.serialization; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.io.InvalidClassException; 024import java.io.ObjectInputStream; 025import java.io.ObjectStreamClass; 026import java.util.regex.Pattern; 027 028import org.apache.commons.io.build.AbstractStreamBuilder; 029import org.apache.commons.io.input.BoundedInputStream; 030 031/** 032 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes. 033 * 034 * <p> 035 * Various accept/reject methods allow for specifying which classes can be deserialized. 036 * </p> 037 * <h2>Deserlizing safely</h2> 038 * <p> 039 * Here is the only way to safely read a HashMap of String keys and Integer values: 040 * </p> 041 * 042 * <pre>{@code 043 * // Defining Object fixture 044 * final HashMap<String, Integer> map1 = new HashMap<>(); 045 * map1.put("1", 1); 046 * // Writing serialized fixture 047 * final byte[] byteArray; 048 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 049 * final ObjectOutputStream oos = new ObjectOutputStream(baos)) { 050 * oos.writeObject(map1); 051 * oos.flush(); 052 * byteArray = baos.toByteArray(); 053 * } 054 * // Deserializing 055 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray); 056 * ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() 057 * .accept(HashMap.class, Number.class, Integer.class) 058 * .setInputStream(bais) 059 * .get()) { 060 * // String.class is automatically accepted 061 * final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject(); 062 * assertEquals(map1, map2); 063 * } 064 * // Reusing a configuration 065 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate() 066 * .accept(HashMap.class, Number.class, Integer.class); 067 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray); 068 * ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() 069 * .setPredicate(predicate) 070 * .setInputStream(bais) 071 * .get()) { 072 * // String.class is automatically accepted 073 * final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject(); 074 * assertEquals(map1, map2); 075 * } 076 * }</pre> 077 * <p> 078 * This design was inspired by a <a href="https://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>. 079 * </p> 080 * <h2>Deserlizing with a size boundary</h2> 081 * <p> 082 * You can further guard your application againt untrusted input by limiting how much data to process using a {@link BoundedInputStream}. 083 * For example: 084 * </p> 085 * <pre>{@code 086 * // Deserializing with a size limit successfully 087 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray); 088 * ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() 089 * .accept(HashMap.class, Number.class, Integer.class) 090 * .setInputStream(BoundedInputStream.builder() 091 * .setMaxCount(10_000) 092 * .setOnMaxCount((max, count) -> { 093 * throw new IllegalArgumentException("Input exceeds limit."); 094 * }) 095 * .setInputStream(bais) 096 * .get()) 097 * .get()) { 098 * // String.class is automatically accepted 099 * 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 */ 121public 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}