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 * http://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; 029 030/** 031 * An {@link ObjectInputStream} that's restricted to deserialize a limited set of classes. 032 * 033 * <p> 034 * Various accept/reject methods allow for specifying which classes can be deserialized. 035 * </p> 036 * <h2>Reading safely</h2> 037 * <p> 038 * Here is the only way to safely read a HashMap of String keys and Integer values: 039 * </p> 040 * 041 * <pre>{@code 042 * // Defining Object fixture 043 * final HashMap<String, Integer> map1 = new HashMap<>(); 044 * map1.put("1", 1); 045 * // Writing serialized fixture 046 * final byte[] byteArray; 047 * try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); 048 * final ObjectOutputStream oos = new ObjectOutputStream(baos)) { 049 * oos.writeObject(map1); 050 * oos.flush(); 051 * byteArray = baos.toByteArray(); 052 * } 053 * // Reading 054 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray); 055 * ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() 056 * .accept(HashMap.class, Number.class, Integer.class) 057 * .setInputStream(bais) 058 * .get()) { 059 * // String.class is automatically accepted 060 * final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject(); 061 * assertEquals(map1, map2); 062 * } 063 * // Reusing a configuration 064 * final ObjectStreamClassPredicate predicate = new ObjectStreamClassPredicate() 065 * .accept(HashMap.class, Number.class, Integer.class); 066 * try (ByteArrayInputStream bais = new ByteArrayInputStream(byteArray); 067 * ValidatingObjectInputStream vois = ValidatingObjectInputStream.builder() 068 * .setPredicate(predicate) 069 * .setInputStream(bais) 070 * .get()) { 071 * // String.class is automatically accepted 072 * final HashMap<String, Integer> map2 = (HashMap<String, Integer>) vois.readObject(); 073 * assertEquals(map1, map2); 074 * } 075 * }</pre> 076 * <p> 077 * Design inspired by a <a href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM DeveloperWorks Article</a>. 078 * </p> 079 * 080 * @since 2.5 081 */ 082public class ValidatingObjectInputStream extends ObjectInputStream { 083 084 // @formatter:off 085 /** 086 * Builds a new {@link ValidatingObjectInputStream}. 087 * 088 * <h2>Using NIO</h2> 089 * <pre>{@code 090 * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder() 091 * .setPath(Paths.get("MyFile.ser")) 092 * .get();} 093 * </pre> 094 * <h2>Using IO</h2> 095 * <pre>{@code 096 * ValidatingObjectInputStream s = ValidatingObjectInputStream.builder() 097 * .setFile(new File("MyFile.ser")) 098 * .get();} 099 * </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 instance. 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 @Override 169 public ValidatingObjectInputStream get() throws IOException { 170 return new ValidatingObjectInputStream(getInputStream(), predicate); 171 } 172 173 /** 174 * Gets the predicate. 175 * 176 * @return the predicate. 177 * @since 2.18.0 178 */ 179 public ObjectStreamClassPredicate getPredicate() { 180 return predicate; 181 } 182 183 /** 184 * Rejects the specified classes for deserialization, even if they are otherwise accepted. 185 * 186 * @param classes Classes to reject 187 * @return this instance. 188 * @since 2.18.0 189 */ 190 public Builder reject(final Class<?>... classes) { 191 predicate.reject(classes); 192 return this; 193 } 194 195 /** 196 * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted. 197 * 198 * @param matcher the matcher to use 199 * @return this instance. 200 * @since 2.18.0 201 */ 202 public Builder reject(final ClassNameMatcher matcher) { 203 predicate.reject(matcher); 204 return this; 205 } 206 207 /** 208 * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted. 209 * 210 * @param pattern standard Java regexp 211 * @return this instance. 212 * @since 2.18.0 213 */ 214 public Builder reject(final Pattern pattern) { 215 predicate.reject(pattern); 216 return this; 217 } 218 219 /** 220 * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted. 221 * 222 * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) 223 * FilenameUtils.wildcardMatch} 224 * @return this instance. 225 * @since 2.18.0 226 */ 227 public Builder reject(final String... patterns) { 228 predicate.reject(patterns); 229 return this; 230 } 231 232 /** 233 * Sets the predicate, null resets to an empty new ObjectStreamClassPredicate. 234 * 235 * @param predicate the predicate. 236 * @return this instance. 237 * @since 2.18.0 238 */ 239 public Builder setPredicate(final ObjectStreamClassPredicate predicate) { 240 this.predicate = predicate != null ? predicate : new ObjectStreamClassPredicate(); 241 return this; 242 } 243 244 } 245 246 /** 247 * Constructs a new {@link Builder}. 248 * 249 * @return a new {@link Builder}. 250 * @since 2.18.0 251 */ 252 public static Builder builder() { 253 return new Builder(); 254 } 255 256 private final ObjectStreamClassPredicate predicate; 257 258 /** 259 * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be 260 * deserialized, as by default no classes are accepted. 261 * 262 * @param input an input stream 263 * @throws IOException if an I/O error occurs while reading stream header 264 * @deprecated Use {@link #builder()}. 265 */ 266 @Deprecated 267 public ValidatingObjectInputStream(final InputStream input) throws IOException { 268 this(input, new ObjectStreamClassPredicate()); 269 } 270 271 /** 272 * Constructs an instance to deserialize the specified input stream. At least one accept method needs to be called to specify which classes can be 273 * deserialized, as by default no classes are accepted. 274 * 275 * @param input an input stream. 276 * @param predicate how to accept and reject classes. 277 * @throws IOException if an I/O error occurs while reading stream header. 278 */ 279 private ValidatingObjectInputStream(final InputStream input, final ObjectStreamClassPredicate predicate) throws IOException { 280 super(input); 281 this.predicate = predicate; 282 } 283 284 /** 285 * Accepts the specified classes for deserialization, unless they are otherwise rejected. 286 * <p> 287 * The reject list takes precedence over the accept list. 288 * </p> 289 * 290 * @param classes Classes to accept 291 * @return this instance. 292 */ 293 public ValidatingObjectInputStream accept(final Class<?>... classes) { 294 predicate.accept(classes); 295 return this; 296 } 297 298 /** 299 * Accepts class names where the supplied ClassNameMatcher matches for deserialization, unless they are otherwise rejected. 300 * <p> 301 * The reject list takes precedence over the accept list. 302 * </p> 303 * 304 * @param matcher a class name matcher to <em>accept</em> objects. 305 * @return this instance. 306 */ 307 public ValidatingObjectInputStream accept(final ClassNameMatcher matcher) { 308 predicate.accept(matcher); 309 return this; 310 } 311 312 /** 313 * Accepts class names that match the supplied pattern for deserialization, unless they are otherwise rejected. 314 * <p> 315 * The reject list takes precedence over the accept list. 316 * </p> 317 * 318 * @param pattern a Pattern for compiled regular expression. 319 * @return this instance. 320 */ 321 public ValidatingObjectInputStream accept(final Pattern pattern) { 322 predicate.accept(pattern); 323 return this; 324 } 325 326 /** 327 * Accepts the wildcard specified classes for deserialization, unless they are otherwise rejected. 328 * <p> 329 * The reject list takes precedence over the accept list. 330 * </p> 331 * 332 * @param patterns Wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) 333 * FilenameUtils.wildcardMatch}. 334 * @return this instance. 335 */ 336 public ValidatingObjectInputStream accept(final String... patterns) { 337 predicate.accept(patterns); 338 return this; 339 } 340 341 /** 342 * Checks that the class name conforms to requirements. 343 * <p> 344 * The reject list takes precedence over the accept list. 345 * </p> 346 * 347 * @param name The class name to test. 348 * @throws InvalidClassException Thrown when a rejected or non-accepted class is found. 349 */ 350 private void checkClassName(final String name) throws InvalidClassException { 351 if (!predicate.test(name)) { 352 invalidClassNameFound(name); 353 } 354 } 355 356 /** 357 * Called to throw {@link InvalidClassException} if an invalid class name is found during deserialization. Can be overridden, for example to log those class 358 * names. 359 * 360 * @param className name of the invalid class. 361 * @throws InvalidClassException Thrown with a message containing the class name. 362 */ 363 protected void invalidClassNameFound(final String className) throws InvalidClassException { 364 throw new InvalidClassException("Class name not accepted: " + className); 365 } 366 367 /** 368 * Delegates to {@link #readObject()} and casts to the generic {@code T}. 369 * 370 * @param <T> The return type. 371 * @return Result from {@link #readObject()}. 372 * @throws ClassNotFoundException Thrown by {@link #readObject()}. 373 * @throws IOException Thrown by {@link #readObject()}. 374 * @throws ClassCastException Thrown when {@link #readObject()} does not match {@code T}. 375 * @since 2.18.0 376 */ 377 @SuppressWarnings("unchecked") 378 public <T> T readObjectCast() throws ClassNotFoundException, IOException { 379 return (T) super.readObject(); 380 } 381 382 /** 383 * Rejects the specified classes for deserialization, even if they are otherwise accepted. 384 * <p> 385 * The reject list takes precedence over the accept list. 386 * </p> 387 * 388 * @param classes Classes to reject. 389 * @return this instance. 390 */ 391 public ValidatingObjectInputStream reject(final Class<?>... classes) { 392 predicate.reject(classes); 393 return this; 394 } 395 396 /** 397 * Rejects class names where the supplied ClassNameMatcher matches for deserialization, even if they are otherwise accepted. 398 * <p> 399 * The reject list takes precedence over the accept list. 400 * </p> 401 * 402 * @param matcher a class name matcher to <em>reject</em> objects. 403 * @return this instance. 404 */ 405 public ValidatingObjectInputStream reject(final ClassNameMatcher matcher) { 406 predicate.reject(matcher); 407 return this; 408 } 409 410 /** 411 * Rejects class names that match the supplied pattern for deserialization, even if they are otherwise accepted. 412 * <p> 413 * The reject list takes precedence over the accept list. 414 * </p> 415 * 416 * @param pattern a Pattern for compiled regular expression. 417 * @return this instance. 418 */ 419 public ValidatingObjectInputStream reject(final Pattern pattern) { 420 predicate.reject(pattern); 421 return this; 422 } 423 424 /** 425 * Rejects the wildcard specified classes for deserialization, even if they are otherwise accepted. 426 * <p> 427 * The reject list takes precedence over the accept list. 428 * </p> 429 * 430 * @param patterns An array of wildcard file name patterns as defined by {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) 431 * FilenameUtils.wildcardMatch} 432 * @return this instance. 433 */ 434 public ValidatingObjectInputStream reject(final String... patterns) { 435 predicate.reject(patterns); 436 return this; 437 } 438 439 @Override 440 protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException { 441 checkClassName(osc.getName()); 442 return super.resolveClass(osc); 443 } 444}