Source code
Description: source: package: org.apache.commons.xml.secure, class: SecureXMLInputFactory

    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one or more
    003 * contributor license agreements.  See the NOTICE file distributed with
    004 * this work for additional information regarding copyright ownership.
    005 * The ASF licenses this file to You under the Apache License, Version 2.0
    006 * (the "License"); you may not use this file except in compliance with
    007 * the License.  You may obtain a copy of the License at
    008 *
    009 *      https://www.apache.org/licenses/LICENSE-2.0
    010 *
    011 * Unless required by applicable law or agreed to in writing, software
    012 * distributed under the License is distributed on an "AS IS" BASIS,
    013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014 * See the License for the specific language governing permissions and
    015 * limitations under the License.
    016 */
    017
    018package org.apache.commons.xml.secure;
    019
    020import java.io.InputStream;
    021import java.io.Reader;
    022import java.lang.invoke.MethodHandle;
    023import java.util.Objects;
    024
    025import javax.xml.stream.EventFilter;
    026import javax.xml.stream.FactoryConfigurationError;
    027import javax.xml.stream.StreamFilter;
    028import javax.xml.stream.XMLEventReader;
    029import javax.xml.stream.XMLInputFactory;
    030import javax.xml.stream.XMLReporter;
    031import javax.xml.stream.XMLResolver;
    032import javax.xml.stream.XMLStreamException;
    033import javax.xml.stream.XMLStreamReader;
    034import javax.xml.stream.util.XMLEventAllocator;
    035import javax.xml.transform.Source;
    036
    037/**
    038 * Creates new, secure {@link XMLInputFactory} instances.
    039 * <p>
    040 * The three universal guarantees on {@link org.apache.commons.xml.secure} apply; StAX exposes no additional vectors beyond them.
    041 * </p>
    042 * <p>
    043 * This class is not itself a {@link XMLInputFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an unsecured
    044 * factory through this class by calling a method such as {@code newDefaultFactory()}. The secure factories are instances of a nested, non-public wrapper class.
    045 * </p>
    046 *
    047 * @see org.apache.commons.xml.secure
    048 */
    049public final class SecureXMLInputFactory {
    050
    051    /**
    052     * {@link XMLInputFactory} wrapper that installs a non-removable {@link FallbackIgnoreXMLResolver} floor on the delegate's entity-resolution hook so the
    053     * caller cannot remove it.
    054     * <p>
    055     * The constructor installs the floor through {@code setXMLResolver}, which every implementation routes external resolution through (Woodstox fans it out to
    056     * both its DTD-subset and entity resolvers). Woodstox keeps one hook outside that fan-out, {@value SecureXMLInputFactory#WSTX_UNDECLARED_ENTITY_RESOLVER},
    057     * which is deliberately left empty: emptying the external subset leaves any entity it declared undeclared, and Woodstox then rejects the reference. The
    058     * rejection is implementation-prescribed and keeps the resource unfetched, like the empty resolution the other implementations produce; a caller who wants
    059     * those references resolved can still set the property, and their resolver lands behind a floor like on every other resolver hook.
    060     * </p>
    061     * <p>
    062     * Every resolver-valued entry point ({@link #setXMLResolver(XMLResolver)}, {@code setProperty(XMLInputFactory.RESOLVER, ...)} and the Woodstox
    063     * {@code com.ctc.wstx.*Resolver} keys) is routed uniformly: a caller who supplies their own {@link FallbackIgnoreXMLResolver} takes control and it is
    064     * passed straight to the delegate; otherwise the caller's resolver is wrapped in a new floor installed on that hook, so the floor cannot be removed by dropping
    065     * the resolver. This matters because Woodstox does not chain resolvers: when a resolver returns {@code null},
    066     * {@code DefaultInputResolver} falls through to fetching the systemId URL itself, so a caller-set resolver that returns {@code null} must still land behind
    067     * the floor. {@link #getXMLResolver()} and {@code getProperty} report the caller's resolver unwrapped.
    068     * </p>
    069     * <p>
    070     * The floor on a hook is replaced rather than mutated, which is what keeps each hook and each reader independent. Woodstox routes {@code setXMLResolver} to
    071     * both its DTD-subset and entity hooks, so one floor object sits on several of them, and it copies that reference into every reader it creates; setting a
    072     * delegate on the object in place would therefore also answer hooks the caller never named, and would change the resolution policy of readers already
    073     * created, including ones parsing on another thread. Installing a new floor leaves both untouched: a hook keeps whatever floor it was given, and a reader
    074     * keeps the one it captured when it was created.
    075     * </p>
    076     */
    077    private static final class Wrapper extends XMLInputFactory {
    078
    079        private static boolean isResolverProperty(final String name) {
    080            return XMLInputFactory.RESOLVER.equals(name)
    081                    || WSTX_DTD_RESOLVER.equals(name)
    082                    || WSTX_ENTITY_RESOLVER.equals(name)
    083                    || WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name);
    084        }
    085
    086
    087        private static XMLResolver unwrap(final XMLResolver resolver) {
    088            return resolver instanceof FallbackIgnoreXMLResolver ? ((FallbackIgnoreXMLResolver) resolver).getDelegate() : resolver;
    089        }
    090
    091        private final XMLInputFactory delegate;
    092
    093        /**
    094         * Constructs a new instance.
    095         *
    096         * @param delegate The delegate to wrap; must not be {@code null}.
    097         * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
    098         */
    099        private Wrapper(final XMLInputFactory delegate) {
    100            this.delegate = Objects.requireNonNull(delegate, "delegate");
    101            delegate.setXMLResolver(new FallbackIgnoreXMLResolver(null));
    102        }
    103
    104        @Override
    105        public XMLEventReader createFilteredReader(final XMLEventReader reader, final EventFilter filter) throws XMLStreamException {
    106            return delegate.createFilteredReader(reader, filter);
    107        }
    108
    109        @Override
    110        public XMLStreamReader createFilteredReader(final XMLStreamReader reader, final StreamFilter filter) throws XMLStreamException {
    111            return delegate.createFilteredReader(reader, filter);
    112        }
    113
    114        @Override
    115        public XMLEventReader createXMLEventReader(final InputStream stream) throws XMLStreamException {
    116            return delegate.createXMLEventReader(stream);
    117        }
    118
    119        @Override
    120        public XMLEventReader createXMLEventReader(final InputStream stream, final String encoding) throws XMLStreamException {
    121            return delegate.createXMLEventReader(stream, encoding);
    122        }
    123
    124        @Override
    125        public XMLEventReader createXMLEventReader(final Reader reader) throws XMLStreamException {
    126            return delegate.createXMLEventReader(reader);
    127        }
    128
    129        @Override
    130        public XMLEventReader createXMLEventReader(final Source source) throws XMLStreamException {
    131            return delegate.createXMLEventReader(source);
    132        }
    133
    134        @Override
    135        public XMLEventReader createXMLEventReader(final String systemId, final InputStream stream) throws XMLStreamException {
    136            return delegate.createXMLEventReader(systemId, stream);
    137        }
    138
    139        @Override
    140        public XMLEventReader createXMLEventReader(final String systemId, final Reader reader) throws XMLStreamException {
    141            return delegate.createXMLEventReader(systemId, reader);
    142        }
    143
    144        @Override
    145        public XMLEventReader createXMLEventReader(final XMLStreamReader reader) throws XMLStreamException {
    146            return delegate.createXMLEventReader(reader);
    147        }
    148
    149        @Override
    150        public XMLStreamReader createXMLStreamReader(final InputStream stream) throws XMLStreamException {
    151            return delegate.createXMLStreamReader(stream);
    152        }
    153
    154        @Override
    155        public XMLStreamReader createXMLStreamReader(final InputStream stream, final String encoding) throws XMLStreamException {
    156            return delegate.createXMLStreamReader(stream, encoding);
    157        }
    158
    159        @Override
    160        public XMLStreamReader createXMLStreamReader(final Reader reader) throws XMLStreamException {
    161            return delegate.createXMLStreamReader(reader);
    162        }
    163
    164        @Override
    165        public XMLStreamReader createXMLStreamReader(final Source source) throws XMLStreamException {
    166            return delegate.createXMLStreamReader(source);
    167        }
    168
    169        @Override
    170        public XMLStreamReader createXMLStreamReader(final String systemId, final InputStream stream) throws XMLStreamException {
    171            return delegate.createXMLStreamReader(systemId, stream);
    172        }
    173
    174        @Override
    175        public XMLStreamReader createXMLStreamReader(final String systemId, final Reader reader) throws XMLStreamException {
    176            return delegate.createXMLStreamReader(systemId, reader);
    177        }
    178
    179
    180        @Override
    181        public XMLEventAllocator getEventAllocator() {
    182            return delegate.getEventAllocator();
    183        }
    184
    185        @Override
    186        public Object getProperty(final String name) {
    187            if (isResolverProperty(name)) {
    188                return unwrap((XMLResolver) delegate.getProperty(name));
    189            }
    190            return delegate.getProperty(name);
    191        }
    192
    193        @Override
    194        public XMLReporter getXMLReporter() {
    195            return delegate.getXMLReporter();
    196        }
    197
    198        @Override
    199        public XMLResolver getXMLResolver() {
    200            return unwrap(delegate.getXMLResolver());
    201        }
    202
    203        @Override
    204        public boolean isPropertySupported(final String name) {
    205            return delegate.isPropertySupported(name);
    206        }
    207
    208        @Override
    209        public void setEventAllocator(final XMLEventAllocator allocator) {
    210            delegate.setEventAllocator(allocator);
    211        }
    212
    213        @Override
    214        public void setProperty(final String name, final Object value) {
    215            // If a resolver property has a value of the wrong type, pass it to the delegate to generate an appropriate exception.
    216            if (isResolverProperty(name) && (value == null || value instanceof XMLResolver)) {
    217                setResolverProperty(name, (XMLResolver) value);
    218            } else {
    219                delegate.setProperty(name, value);
    220            }
    221        }
    222
    223        /**
    224         * Routes a caller-set resolver for the property {@code name} behind the floor currently installed on that hook.
    225         *
    226         * @param name     The resolver-valued property being set.
    227         * @param resolver The caller's resolver, or their own {@link FallbackIgnoreXMLResolver} to take control.
    228         */
    229        private void setResolverProperty(final String name, final XMLResolver resolver) {
    230            if (resolver instanceof FallbackIgnoreXMLResolver) {
    231                // The caller supplies their own floor: hand it to the delegate as-is.
    232                delegate.setProperty(name, resolver);
    233            } else {
    234                // A fresh floor for this hook rather than a new delegate on the floor already there: Woodstox puts one floor object on several hooks and copies
    235                // the reference into every reader it creates, so mutating it would reach hooks, and readers already parsing, that this call never named.
    236                delegate.setProperty(name, new FallbackIgnoreXMLResolver(resolver));
    237            }
    238        }
    239
    240        @Override
    241        public void setXMLReporter(final XMLReporter reporter) {
    242            delegate.setXMLReporter(reporter);
    243        }
    244
    245        @Override
    246        public void setXMLResolver(final XMLResolver resolver) {
    247            setResolverProperty(XMLInputFactory.RESOLVER, resolver);
    248        }
    249    }
    250    /** Woodstox property: resolver consulted for the external DTD subset. */
    251    static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
    252    /** Woodstox property: resolver consulted for declared external general entities. */
    253    static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
    254    /** Woodstox property: resolver consulted for undeclared entity references. */
    255    static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
    256
    257    /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultFactory()}. */
    258    private static final String JDK_XML_INPUT_FACTORY = "com.sun.xml.internal.stream.XMLInputFactoryImpl";
    259
    260    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(XMLInputFactory.class, "newDefaultFactory");
    261
    262    /**
    263     * Returns a new, secure {@link XMLInputFactory} of the system-default implementation.
    264     * <p>
    265     * Obtained from {@code XMLInputFactory.newDefaultFactory()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in
    266     * implementation directly on Java 8.
    267     * </p>
    268     *
    269     * @return A secure factory.
    270     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    271     * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultFactory()} nor the JDK's built-in implementation
    272     *                                   (for example Android).
    273     */
    274    public static XMLInputFactory newDefaultFactory() {
    275        if (MH_newDefaultInstance != null) {
    276            return secure(MethodHandleFactory.invokeExact(() -> (XMLInputFactory) MH_newDefaultInstance.invokeExact(), FactoryConfigurationError.class));
    277        }
    278        try {
    279            // Java 8: the method does not exist, and XMLInputFactory has no class-name-taking lookup; instantiate the JDK's built-in default directly.
    280            return secure((XMLInputFactory) Class.forName(JDK_XML_INPUT_FACTORY).getConstructor().newInstance());
    281        } catch (final ReflectiveOperationException e) {
    282            // Where the class does not exist either (for example Android), report the miss like any StAX factory lookup: with FactoryConfigurationError.
    283            throw new FactoryConfigurationError(e, "Neither XMLInputFactory.newDefaultFactory() nor " + JDK_XML_INPUT_FACTORY + " is available");
    284        }
    285    }
    286
    287    /**
    288     * Returns a new, secure {@link XMLInputFactory}, like {@link XMLInputFactory#newFactory()}.
    289     *
    290     * @return A secure factory.
    291     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    292     * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded.
    293     */
    294    public static XMLInputFactory newFactory() {
    295        // XMLInputFactory.newInstance, not newFactory: the same specified lookup, but Android's StAX API predates newFactory.
    296        return secure(XMLInputFactory.newInstance());
    297    }
    298
    299    /**
    300     * Returns a new, secure {@link XMLInputFactory} resolved from the given factory id.
    301     *
    302     * @param factoryId   The name of the factory to find; a system property or service id to look up, not the class name of the implementation.
    303     * @param classLoader The class loader used in the lookup; {@code null} means the current thread's context class loader.
    304     * @return A secure factory.
    305     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    306     * @throws FactoryConfigurationError Thrown in case of a service configuration error or if the implementation is not available or cannot be instantiated.
    307     * @throws NullPointerException      Thrown if {@code factoryId} is {@code null}.
    308     */
    309    public static XMLInputFactory newFactory(final String factoryId, final ClassLoader classLoader) {
    310        return secure(XMLInputFactory.newFactory(factoryId, classLoader));
    311    }
    312
    313    /**
    314     * Returns a new, secure {@link XMLInputFactory}.
    315     *
    316     * @return A secure factory.
    317     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    318     * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded.
    319     */
    320    public static XMLInputFactory newInstance() {
    321        return secure(XMLInputFactory.newInstance());
    322    }
    323
    324    /**
    325     * Capability-driven secure for any {@link XMLInputFactory} (StAX) on the classpath.
    326     *
    327     * <p>One recipe covers both the JDK Zephyr and Woodstox: the wrapper installs a non-removable {@link FallbackIgnoreXMLResolver} floor on
    328     * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see the wrapper's
    329     * Javadoc for the per-implementation hook routing.</p>
    330     *
    331     * @param factory The factory to secure; never {@code null}.
    332     * @return a secure factory.
    333     */
    334    static XMLInputFactory secure(final XMLInputFactory factory) {
    335        // The wrapper installs the non-removable ignore-all resolver floor that resolves every external DTD and entity to empty content.
    336        return new Wrapper(factory);
    337    }
    338
    339    private SecureXMLInputFactory() {
    340        // static only
    341    }
    342}