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 }