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