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.lang.invoke.MethodHandle;
021import java.util.Objects;
022
023import javax.xml.XMLConstants;
024import javax.xml.parsers.FactoryConfigurationError;
025import javax.xml.transform.Source;
026import javax.xml.validation.Schema;
027import javax.xml.validation.SchemaFactory;
028import javax.xml.validation.SchemaFactoryConfigurationError;
029import javax.xml.validation.Validator;
030
031import org.w3c.dom.ls.LSResourceResolver;
032import org.xml.sax.ErrorHandler;
033import org.xml.sax.SAXException;
034import org.xml.sax.SAXNotRecognizedException;
035import org.xml.sax.SAXNotSupportedException;
036
037/**
038 * Creates new, secure {@link SchemaFactory} instances.
039 * <p>
040 * Beyond the three universal guarantees on {@link org.apache.commons.xml.secure}:
041 * </p>
042 * <ul>
043 * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation,</li>
044 * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation, and</li>
045 * <li>the content model a schema expands into is bounded, on every implementation offering a limit for it. A loader expands a repeated particle while building
046 * the DFA, so a compact schema carrying a large {@code maxOccurs} would otherwise exhaust memory or CPU (see Xerces'
047 * <a href="https://xerces.apache.org/xerces2-j/properties.html#security-manager">security manager</a>, which caps that expansion at 3,000 nodes).</li>
048 * </ul>
049 * <p>
050 * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the resulting
051 * {@link javax.xml.validation.Schema}.
052 * </p>
053 * <p>
054 * This class is not itself a {@link SchemaFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an unsecured
055 * factory through this class by calling a method such as {@code newDefaultInstance()}. The secure factories are instances of a nested, non-public wrapper
056 * class.
057 * </p>
058 *
059 * @see org.apache.commons.xml.secure
060 */
061public final class SecureSchemaFactory {
062
063    /**
064     * Capability-driven secure wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached
065     * by {@link SecureSchemaFactory#newInstance(String)}; there is no per-implementation branching and no limit configuration on the factory itself beyond
066     * {@code FEATURE_SECURE_PROCESSING}.
067     *
068     * <p>Three layers cooperate:</p>
069     * <ol>
070     *   <li>{@link SecureSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking
071     *       {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point
072     *       through {@link SecureSAXParserFactory#secure(Source, boolean)}.</li>
073     *   <li>{@link SecureSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking
074     *       {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li>
075     *   <li>{@link SecureValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li>
076     * </ol>
077     *
078     * <p>
079     * The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a
080     * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. One limit it cannot
081     * supply is content-model expansion: a large {@code maxOccurs} is expanded by the schema loader when it builds the DFA, after parsing and without the
082     * reader, so {@code FEATURE_SECURE_PROCESSING} is set on the factory as well, which is what installs that bound on external Xerces (the stock JDK applies
083     * it unconditionally). The JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties are still not set explicitly: the resolver floor already blocks the same fetches on
084     * every implementation, and the JDK 8 {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant
085     * access. The floor is a non-removable lower bound: a caller-set {@link LSResourceResolver} is routed through it (opting a specific lookup in by returning a
086     * non-{@code null} result) rather than replacing it, so the securing (or the floor) cannot be dropped by swapping the resolver.
087     * </p>
088     */
089    private static final class Wrapper extends SchemaFactory {
090
091        private final SchemaFactory delegate;
092
093
094        private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null);
095
096        /**
097         * Constructs a new instance.
098         *
099         * @param delegate The delegate to wrap; must not be {@code null}.
100         * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
101         */
102        private Wrapper(final SchemaFactory delegate) {
103            this.delegate = Objects.requireNonNull(delegate, "delegate");
104            // Content-model expansion happens in the schema loader, after parsing, so the injected reader's limits cannot reach it.
105            SecureSchemaFactory.setFeature(delegate, XMLConstants.FEATURE_SECURE_PROCESSING, true);
106            // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader).
107            delegate.setResourceResolver(floor);
108        }
109
110        @Override
111        public ErrorHandler getErrorHandler() {
112            return delegate.getErrorHandler();
113        }
114
115        @Override
116        public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
117            return delegate.getFeature(name);
118        }
119
120        @Override
121        public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
122            return delegate.getProperty(name);
123        }
124
125        @Override
126        public LSResourceResolver getResourceResolver() {
127            return floor.getDelegate();
128        }
129
130        @Override
131        public boolean isSchemaLanguageSupported(final String schemaLanguage) {
132            return delegate.isSchemaLanguageSupported(schemaLanguage);
133        }
134
135        @Override
136        public Schema newSchema() throws SAXException {
137            return new SecureSchema(delegate.newSchema(), overrideDefaultParser());
138        }
139
140        /**
141         * {@inheritDoc}
142         *
143         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
144         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
145         */
146        @Override
147        public Schema newSchema(final Source[] schemas) throws SAXException {
148            return new SecureSchema(delegate.newSchema(secure(schemas)), overrideDefaultParser());
149        }
150
151        /**
152         * Tests whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}.
153         *
154         * <p>The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or
155         * {@code false}.</p>
156         *
157         * @return {@code true} if parsers should be created via {@code newInstance()}.
158         */
159        private boolean overrideDefaultParser() {
160            try {
161                return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER);
162            } catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
163                return true;
164            }
165        }
166
167        /**
168         * Secures every schema source through {@link SecureSAXParserFactory#secure(Source, boolean)}.
169         *
170         * @param schemas The schema sources to secure; must not be {@code null}.
171         * @return a new array of secure sources.
172         * @throws IllegalStateException     Thrown if the underlying implementation cannot provide a secure reader.
173         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
174         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
175         */
176        private Source[] secure(final Source[] schemas) {
177            final Source[] secure = new Source[schemas.length];
178            final boolean overrideDefaultParser = overrideDefaultParser();
179            for (int i = 0; i < schemas.length; i++) {
180                secure[i] = SecureSAXParserFactory.secure(schemas[i], overrideDefaultParser);
181            }
182            return secure;
183        }
184
185        @Override
186        public void setErrorHandler(final ErrorHandler errorHandler) {
187            delegate.setErrorHandler(errorHandler);
188        }
189
190        @Override
191        public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
192            delegate.setFeature(name, value);
193        }
194
195
196        @Override
197        public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
198            delegate.setProperty(name, object);
199        }
200
201        @Override
202        public void setResourceResolver(final LSResourceResolver resourceResolver) {
203            // Route a caller resolver through the floor instead of replacing it, so the ignore-all lower bound cannot be removed.
204            floor.setDelegate(resourceResolver);
205        }
206    }
207
208    /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
209    private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory";
210
211    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(SchemaFactory.class, "newDefaultInstance");
212
213    /**
214     * Returns a new, secure {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0.
215     * <p>
216     * Obtained from {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), by instantiating the JDK's built-in
217     * implementation directly on Java 8, and by the standard {@link #newInstance(String)} lookup where the platform provides neither (for example Android,
218     * whose lookup falls back to exactly the Xerces implementation this library recognizes).
219     * </p>
220     *
221     * @return A secure factory.
222     * @throws IllegalStateException    Thrown if a required secure setting cannot be applied to the underlying implementation.
223     * @throws IllegalArgumentException Thrown from the {@link #newInstance(String)} lookup this method falls back to on a platform that provides neither
224     *                                 {@code newDefaultInstance()} nor the JDK's built-in implementation (for example Android).
225     */
226    public static SchemaFactory newDefaultInstance() {
227        if (MH_newDefaultInstance != null) {
228            return secure(MethodHandleFactory.invokeExact(() -> (SchemaFactory) MH_newDefaultInstance.invokeExact(), SchemaFactoryConfigurationError.class));
229        }
230        try {
231            // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead.
232            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null);
233        } catch (final IllegalArgumentException e) {
234            // Neither exists (for example Android): degrade to the regular lookup, whose Android fallback is exactly the Xerces implementation.
235            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
236        }
237    }
238
239    /**
240     * Returns a new, secure {@link SchemaFactory} for the given schema language.
241     *
242     * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
243     * @return A secure factory.
244     * @throws IllegalArgumentException        Thrown if no implementation of the schema language is available.
245     * @throws NullPointerException            Thrown if {@code schemaLanguage} is {@code null}.
246     * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered.
247     */
248    public static SchemaFactory newInstance(final String schemaLanguage) {
249        return secure(SchemaFactory.newInstance(schemaLanguage));
250    }
251
252    /**
253     * Returns a new, secure {@link SchemaFactory} of the given implementation class.
254     *
255     * @param schemaLanguage   The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
256     * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation.
257     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
258     * @return A secure factory.
259     * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does
260     *                                  not support {@code schemaLanguage}.
261     * @throws NullPointerException     Thrown if {@code schemaLanguage} is {@code null}.
262     */
263    public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) {
264        return secure(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader));
265    }
266
267    /**
268     * Secures a {@link SchemaFactory}.
269     *
270     * <p>Unlike the other factory types there is no per-implementation branching: schema compilation and validation reach external resources only through the
271     * resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on every implementation. The reader used to parse schema
272     * and instance documents is secured separately, through {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}; the factory carries
273     * {@code FEATURE_SECURE_PROCESSING} for the one limit that reader cannot supply, the loader's content-model expansion.</p>
274     *
275     * @param factory The factory to secure; never {@code null}.
276     * @return a secure factory.
277     */
278    static SchemaFactory secure(final SchemaFactory factory) {
279        return new Wrapper(factory);
280    }
281
282    /**
283     * Sets a feature on the delegate, failing closed: an implementation that cannot accept it yields no factory rather than an unsecured one.
284     *
285     * @param factory The factory to configure; never {@code null}.
286     * @param feature The feature name.
287     * @param value   The value to set.
288     * @throws SecureException Thrown if the implementation rejects the feature.
289     */
290    private static void setFeature(final SchemaFactory factory, final String feature, final boolean value) {
291        try {
292            factory.setFeature(feature, value);
293        } catch (final Exception e) {
294            throw SecureException.featureFailed(feature, factory, e);
295        }
296    }
297
298    private SecureSchemaFactory() {
299        // static only
300    }
301}