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>
069     * Three layers cooperate:
070     * </p>
071     * <ol>
072     *   <li>{@link SecureSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking
073     *       {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point
074     *       through {@link SecureSAXParserFactory#secure(Source, boolean)}.</li>
075     *   <li>{@link SecureSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking
076     *       {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li>
077     *   <li>{@link SecureValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li>
078     * </ol>
079     *
080     * <p>
081     * The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing
082     * limits, so a
083     * 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
084     * 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
085     * 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
086     * it unconditionally). The JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties are still not set explicitly: the resolver floor already blocks the same fetches
087     * on
088     * 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
089     * 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
090     * a non-{@code null} result) rather than replacing it, so the securing (or the floor) cannot be dropped by swapping the resolver.
091     * </p>
092     */
093    private static final class Wrapper extends SchemaFactory {
094
095        private final SchemaFactory delegate;
096
097
098        private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null);
099
100        /**
101         * Constructs a new instance.
102         *
103         * @param delegate The delegate to wrap; must not be {@code null}.
104         * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
105         */
106        private Wrapper(final SchemaFactory delegate) {
107            this.delegate = Objects.requireNonNull(delegate, "delegate");
108            // Content-model expansion happens in the schema loader, after parsing, so the injected reader's limits cannot reach it.
109            SecureSchemaFactory.setFeature(delegate, XMLConstants.FEATURE_SECURE_PROCESSING, true);
110            // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader).
111            delegate.setResourceResolver(floor);
112        }
113
114        @Override
115        public ErrorHandler getErrorHandler() {
116            return delegate.getErrorHandler();
117        }
118
119        @Override
120        public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
121            return delegate.getFeature(name);
122        }
123
124        @Override
125        public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
126            return delegate.getProperty(name);
127        }
128
129        @Override
130        public LSResourceResolver getResourceResolver() {
131            return floor.getDelegate();
132        }
133
134        @Override
135        public boolean isSchemaLanguageSupported(final String schemaLanguage) {
136            return delegate.isSchemaLanguageSupported(schemaLanguage);
137        }
138
139        @Override
140        public Schema newSchema() throws SAXException {
141            return new SecureSchema(delegate.newSchema(), overrideDefaultParser());
142        }
143
144        /**
145         * {@inheritDoc}
146         *
147         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
148         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
149         */
150        @Override
151        public Schema newSchema(final Source[] schemas) throws SAXException {
152            return new SecureSchema(delegate.newSchema(secure(schemas)), overrideDefaultParser());
153        }
154
155        /**
156         * Tests whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}.
157         *
158         * <p>
159         * The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or
160         * {@code false}.
161         * </p>
162         *
163         * @return {@code true} if parsers should be created via {@code newInstance()}.
164         */
165        private boolean overrideDefaultParser() {
166            try {
167                return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER);
168            } catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
169                return true;
170            }
171        }
172
173        /**
174         * Secures every schema source through {@link SecureSAXParserFactory#secure(Source, boolean)}.
175         *
176         * @param schemas The schema sources to secure; must not be {@code null}.
177         * @return a new array of secure sources.
178         * @throws IllegalStateException     Thrown if the underlying implementation cannot provide a secure reader.
179         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
180         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
181         */
182        private Source[] secure(final Source[] schemas) {
183            final Source[] secure = new Source[schemas.length];
184            final boolean overrideDefaultParser = overrideDefaultParser();
185            for (int i = 0; i < schemas.length; i++) {
186                secure[i] = SecureSAXParserFactory.secure(schemas[i], overrideDefaultParser);
187            }
188            return secure;
189        }
190
191        @Override
192        public void setErrorHandler(final ErrorHandler errorHandler) {
193            delegate.setErrorHandler(errorHandler);
194        }
195
196        @Override
197        public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
198            delegate.setFeature(name, value);
199        }
200
201
202        @Override
203        public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
204            delegate.setProperty(name, object);
205        }
206
207        @Override
208        public void setResourceResolver(final LSResourceResolver resourceResolver) {
209            // Route a caller resolver through the floor instead of replacing it, so the ignore-all lower bound cannot be removed.
210            floor.setDelegate(resourceResolver);
211        }
212    }
213
214    /**
215     * Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}.
216     */
217    private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory";
218
219    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(SchemaFactory.class, "newDefaultInstance");
220
221    /**
222     * Returns a new, secure {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0.
223     * <p>
224     * Obtained from {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), by instantiating the JDK's built-in
225     * implementation directly on Java 8, and by the standard {@link #newInstance(String)} lookup where the platform provides neither (for example, Android,
226     * whose lookup falls back to exactly the Xerces implementation this library recognizes).
227     * </p>
228     *
229     * @return A secure factory.
230     * @throws IllegalStateException    Thrown if a required secure setting cannot be applied to the underlying implementation.
231     * @throws IllegalArgumentException Thrown from the {@link #newInstance(String)} lookup this method falls back to on a platform that provides neither
232     *                                 {@code newDefaultInstance()} nor the JDK's built-in implementation (for example, Android).
233     */
234    public static SchemaFactory newDefaultInstance() {
235        if (MH_newDefaultInstance != null) {
236            return secure(MethodHandleFactory.invokeExact(() -> (SchemaFactory) MH_newDefaultInstance.invokeExact(), SchemaFactoryConfigurationError.class));
237        }
238        try {
239            // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead.
240            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null);
241        } catch (final IllegalArgumentException e) {
242            // Neither exists (for example Android): degrade to the regular lookup, whose Android fallback is exactly the Xerces implementation.
243            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
244        }
245    }
246
247    /**
248     * Returns a new, secure {@link SchemaFactory} for the given schema language.
249     *
250     * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
251     * @return A secure factory.
252     * @throws IllegalArgumentException        Thrown if no implementation of the schema language is available.
253     * @throws NullPointerException            Thrown if {@code schemaLanguage} is {@code null}.
254     * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered.
255     */
256    public static SchemaFactory newInstance(final String schemaLanguage) {
257        return secure(SchemaFactory.newInstance(schemaLanguage));
258    }
259
260    /**
261     * Returns a new, secure {@link SchemaFactory} of the given implementation class.
262     *
263     * @param schemaLanguage   The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
264     * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation.
265     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
266     * @return A secure factory.
267     * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does
268     *                                  not support {@code schemaLanguage}.
269     * @throws NullPointerException     Thrown if {@code schemaLanguage} is {@code null}.
270     */
271    public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) {
272        return secure(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader));
273    }
274
275    /**
276     * Secures a {@link SchemaFactory}.
277     *
278     * <p>
279     * Unlike the other factory types, there is no per-implementation branching: schema compilation and validation reach external resources only through the
280     * 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
281     * and instance documents is secured separately, through {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}; the factory carries
282     * {@code FEATURE_SECURE_PROCESSING} for the one limit that reader cannot supply, the loader's content-model expansion.
283     * </p>
284     *
285     * @param factory The factory to secure; never {@code null}.
286     * @return a secure factory.
287     */
288    static SchemaFactory secure(final SchemaFactory factory) {
289        return new Wrapper(factory);
290    }
291
292    /**
293     * Sets a feature on the delegate, failing closed: an implementation that cannot accept it yields no factory rather than an unsecured one.
294     *
295     * @param factory The factory to configure; never {@code null}.
296     * @param feature The feature name.
297     * @param value   The value to set.
298     * @throws SecureException Thrown if the implementation rejects the feature.
299     */
300    private static void setFeature(final SchemaFactory factory, final String feature, final boolean value) {
301        try {
302            factory.setFeature(feature, value);
303        } catch (final Exception e) {
304            throw SecureException.featureFailed(feature, factory, e);
305        }
306    }
307
308    private SecureSchemaFactory() {
309        // static only
310    }
311}