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.ArrayList;
027import java.util.List;
028import java.util.regex.Pattern;
029import java.util.stream.Stream;
030
031/**
032 * An {@link ObjectInputStream} that's restricted to deserialize
033 * a limited set of classes.
034 *
035 * <p>
036 * Various accept/reject methods allow for specifying which classes
037 * can be deserialized.
038 * </p>
039 *
040 * <p>
041 * Design inspired by <a
042 * href="http://www.ibm.com/developerworks/library/se-lookahead/">IBM
043 * DeveloperWorks Article</a>.
044 * </p>
045 */
046public class ValidatingObjectInputStream extends ObjectInputStream {
047    private final List<ClassNameMatcher> acceptMatchers = new ArrayList<>();
048    private final List<ClassNameMatcher> rejectMatchers = new ArrayList<>();
049
050    /**
051     * Constructs an object to deserialize the specified input stream.
052     * At least one accept method needs to be called to specify which
053     * classes can be deserialized, as by default no classes are
054     * accepted.
055     *
056     * @param input an input stream
057     * @throws IOException if an I/O error occurs while reading stream header
058     */
059    public ValidatingObjectInputStream(final InputStream input) throws IOException {
060        super(input);
061    }
062
063    /**
064     * Accept the specified classes for deserialization, unless they
065     * are otherwise rejected.
066     *
067     * @param classes Classes to accept
068     * @return this object
069     */
070    public ValidatingObjectInputStream accept(final Class<?>... classes) {
071        Stream.of(classes).map(c -> new FullClassNameMatcher(c.getName())).forEach(acceptMatchers::add);
072        return this;
073    }
074
075    /**
076     * Accept class names where the supplied ClassNameMatcher matches for
077     * deserialization, unless they are otherwise rejected.
078     *
079     * @param m the matcher to use
080     * @return this object
081     */
082    public ValidatingObjectInputStream accept(final ClassNameMatcher m) {
083        acceptMatchers.add(m);
084        return this;
085    }
086
087    /**
088     * Accept class names that match the supplied pattern for
089     * deserialization, unless they are otherwise rejected.
090     *
091     * @param pattern standard Java regexp
092     * @return this object
093     */
094    public ValidatingObjectInputStream accept(final Pattern pattern) {
095        acceptMatchers.add(new RegexpClassNameMatcher(pattern));
096        return this;
097    }
098
099    /**
100     * Accept the wildcard specified classes for deserialization,
101     * unless they are otherwise rejected.
102     *
103     * @param patterns Wildcard file name patterns as defined by
104     *                  {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch}
105     * @return this object
106     */
107    public ValidatingObjectInputStream accept(final String... patterns) {
108        Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(acceptMatchers::add);
109        return this;
110    }
111
112    /**
113     * Checks that the class name conforms to requirements.
114     *
115     * @param name The class name
116     * @throws InvalidClassException when a non-accepted class is encountered
117     */
118    private void checkClassName(final String name) throws InvalidClassException {
119        // Reject has precedence over accept
120        for (final ClassNameMatcher m : rejectMatchers) {
121            if (m.matches(name)) {
122                invalidClassNameFound(name);
123            }
124        }
125
126        boolean ok = false;
127        for (final ClassNameMatcher m : acceptMatchers) {
128            if (m.matches(name)) {
129                ok = true;
130                break;
131            }
132        }
133        if (!ok) {
134            invalidClassNameFound(name);
135        }
136    }
137
138    /**
139     * Called to throw {@link InvalidClassException} if an invalid
140     * class name is found during deserialization. Can be overridden, for example
141     * to log those class names.
142     *
143     * @param className name of the invalid class
144     * @throws InvalidClassException if the specified class is not allowed
145     */
146    protected void invalidClassNameFound(final String className) throws InvalidClassException {
147        throw new InvalidClassException("Class name not accepted: " + className);
148    }
149
150    /**
151     * Reject the specified classes for deserialization, even if they
152     * are otherwise accepted.
153     *
154     * @param classes Classes to reject
155     * @return this object
156     */
157    public ValidatingObjectInputStream reject(final Class<?>... classes) {
158        Stream.of(classes).map(c -> new FullClassNameMatcher(c.getName())).forEach(rejectMatchers::add);
159        return this;
160    }
161
162    /**
163     * Reject class names where the supplied ClassNameMatcher matches for
164     * deserialization, even if they are otherwise accepted.
165     *
166     * @param m the matcher to use
167     * @return this object
168     */
169    public ValidatingObjectInputStream reject(final ClassNameMatcher m) {
170        rejectMatchers.add(m);
171        return this;
172    }
173
174    /**
175     * Reject class names that match the supplied pattern for
176     * deserialization, even if they are otherwise accepted.
177     *
178     * @param pattern standard Java regexp
179     * @return this object
180     */
181    public ValidatingObjectInputStream reject(final Pattern pattern) {
182        rejectMatchers.add(new RegexpClassNameMatcher(pattern));
183        return this;
184    }
185
186    /**
187     * Reject the wildcard specified classes for deserialization,
188     * even if they are otherwise accepted.
189     *
190     * @param patterns Wildcard file name patterns as defined by
191     *                  {@link org.apache.commons.io.FilenameUtils#wildcardMatch(String, String) FilenameUtils.wildcardMatch}
192     * @return this object
193     */
194    public ValidatingObjectInputStream reject(final String... patterns) {
195        Stream.of(patterns).map(WildcardClassNameMatcher::new).forEach(rejectMatchers::add);
196        return this;
197    }
198
199    @Override
200    protected Class<?> resolveClass(final ObjectStreamClass osc) throws IOException, ClassNotFoundException {
201        checkClassName(osc.getName());
202        return super.resolveClass(osc);
203    }
204}