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