SecureSchemaFactory.java

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.xml.secure;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.util.Objects;

import javax.xml.XMLConstants;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.transform.Source;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.SchemaFactoryConfigurationError;
import javax.xml.validation.Validator;

import org.w3c.dom.ls.LSResourceResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;

/**
 * Creates new, secure {@link SchemaFactory} instances.
 * <p>
 * Beyond the three universal guarantees on {@link org.apache.commons.xml.secure}:
 * </p>
 * <ul>
 * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and</li>
 * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.</li>
 * </ul>
 * <p>
 * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the
 * resulting {@link javax.xml.validation.Schema}.
 * </p>
 * <p>
 * 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
 * by calling an inherited method such as {@code newDefaultInstance()}. The secure factories are instances of a nested, non-public wrapper class.
 * </p>
 *
 * @see org.apache.commons.xml.secure
 */
public final class SecureSchemaFactory {

    /**
     * Capability-driven secure wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached
     * by {@link SecureSchemaFactory#newInstance(String)}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the
     * factory itself.
     *
     * <p>Three layers cooperate:</p>
     * <ol>
     *   <li>{@link SecureSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking
     *       {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point
     *       through {@link SecureSAXParserFactory#secure(Source, boolean)}.</li>
     *   <li>{@link SecureSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking
     *       {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li>
     *   <li>{@link SecureValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li>
     * </ol>
     *
     * <p>
     * The secure reader supplied by {@link SecureSAXParserFactory#secure(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a
     * 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
     * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the resolver floor already blocks the same fetches on 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 the 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 non-{@code null} result) rather than
     * replacing it, so secure cannot be dropped by swapping the resolver.
     * </p>
     *
     * @see org.apache.commons.xml.secure
     */
    private static final class Wrapper extends SchemaFactory {

        private final SchemaFactory delegate;


        private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null);

        /**
         * Constructs a new instance.
         *
         * @param delegate the delegate to wrap; must not be {@code null}.
         * @throws NullPointerException if {@code delegate} is {@code null}.
         */
        private Wrapper(final SchemaFactory delegate) {
            this.delegate = Objects.requireNonNull(delegate, "delegate");
            // Compile-time block for xs:import/include/redefine; the wrappers carry the rest (per-product resolver, source rewriting, limits via the reader).
            delegate.setResourceResolver(floor);
        }

        @Override
        public ErrorHandler getErrorHandler() {
            return delegate.getErrorHandler();
        }

        @Override
        public boolean getFeature(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
            return delegate.getFeature(name);
        }

        @Override
        public Object getProperty(final String name) throws SAXNotRecognizedException, SAXNotSupportedException {
            return delegate.getProperty(name);
        }

        @Override
        public LSResourceResolver getResourceResolver() {
            return floor.getDelegate();
        }

        @Override
        public boolean isSchemaLanguageSupported(final String schemaLanguage) {
            return delegate.isSchemaLanguageSupported(schemaLanguage);
        }

        @Override
        public Schema newSchema() throws SAXException {
            return new SecureSchema(delegate.newSchema(), overrideDefaultParser());
        }

        /**
         * {@inheritDoc}
         *
         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
         */
        @Override
        public Schema newSchema(final Source[] schemas) throws SAXException {
            return new SecureSchema(delegate.newSchema(secure(schemas)), overrideDefaultParser());
        }

        /**
         * Checks whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}.
         *
         * <p>The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or
         * {@code false}.</p>
         *
         * @return {@code true} if parsers should be created via {@code newInstance()}.
         */
        private boolean overrideDefaultParser() {
            try {
                return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER);
            } catch (final SAXNotRecognizedException | SAXNotSupportedException e) {
                return true;
            }
        }

        /**
         * Secures every schema source through {@link SecureSAXParserFactory#secure(Source, boolean)}.
         *
         * @param schemas the schema sources to secure; must not be {@code null}.
         * @return a new array of secure sources.
         * @throws IllegalStateException     Thrown if the underlying implementation cannot provide a secure reader.
         * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service
         *                                   configuration error} or if the implementation is not available or cannot be instantiated.
         */
        private Source[] secure(final Source[] schemas) {
            final Source[] secure = new Source[schemas.length];
            final boolean overrideDefaultParser = overrideDefaultParser();
            for (int i = 0; i < schemas.length; i++) {
                secure[i] = SecureSAXParserFactory.secure(schemas[i], overrideDefaultParser);
            }
            return secure;
        }

        @Override
        public void setErrorHandler(final ErrorHandler errorHandler) {
            delegate.setErrorHandler(errorHandler);
        }

        @Override
        public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException {
            delegate.setFeature(name, value);
        }


        @Override
        public void setProperty(final String name, final Object object) throws SAXNotRecognizedException, SAXNotSupportedException {
            delegate.setProperty(name, object);
        }

        @Override
        public void setResourceResolver(final LSResourceResolver resourceResolver) {
            // Route a caller resolver through the floor instead of replacing it, so the ignore-all lower bound cannot be removed.
            floor.setDelegate(resourceResolver);
        }
    }

    /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
    private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory";

    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(SchemaFactory.class, "newDefaultInstance",
            MethodType.methodType(SchemaFactory.class));

    /**
     * Returns a new, secure {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0.
     * <p>
     * Obtained as by {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), by instantiating the JDK's built-in
     * implementation directly on Java 8, and by the standard {@link #newInstance(String)} lookup where the platform provides neither (for example Android,
     * whose lookup falls back to exactly the Xerces implementation this library recognizes).
     * </p>
     *
     * @return A secure factory.
     * @throws IllegalStateException    Thrown if a required secure setting cannot be applied to the underlying implementation.
     * @throws IllegalArgumentException Thrown from the {@link #newInstance(String)} lookup this method falls back to on a platform that provides neither
     *                                 {@code newDefaultInstance()} nor the JDK's built-in implementation (for example Android).
     */
    public static SchemaFactory newDefaultInstance() {
        if (MH_newDefaultInstance != null) {
            return secure(MethodHandleFactory.invokeExact(() -> (SchemaFactory) MH_newDefaultInstance.invokeExact(), SchemaFactoryConfigurationError.class));
        }
        try {
            // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead.
            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null);
        } catch (final IllegalArgumentException e) {
            // Neither exists (for example Android): degrade to the regular lookup, whose Android fallback is exactly the Xerces implementation.
            return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        }
    }

    /**
     * Returns a new, secure {@link SchemaFactory} for the given schema language.
     *
     * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
     * @return A secure factory.
     * @throws IllegalArgumentException        Thrown if no implementation of the schema language is available.
     * @throws NullPointerException            Thrown if {@code schemaLanguage} is {@code null}.
     * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered.
     */
    public static SchemaFactory newInstance(final String schemaLanguage) {
        return secure(SchemaFactory.newInstance(schemaLanguage));
    }

    /**
     * Returns a new, secure {@link SchemaFactory} of the given implementation class.
     *
     * @param schemaLanguage   The schema language, as accepted by {@link SchemaFactory#newInstance(String)}.
     * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation.
     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
     * @return A secure factory.
     * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does
     *                                  not support {@code schemaLanguage}.
     * @throws NullPointerException     Thrown if {@code schemaLanguage} is {@code null}.
     */
    public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) {
        return secure(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader));
    }

    /**
     * Secures a {@link SchemaFactory}.
     *
     * <p>Unlike the other factory types there is no per-implementation branching and no feature or limit configuration on the factory itself: schema compilation
     * 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
     * every implementation. The reader used to parse schema and instance documents is secure separately, through
     * {@link SecureSAXParserFactory#secure(javax.xml.transform.Source, boolean)}.</p>
     *
     * @param factory the factory to secure; never {@code null}.
     * @return a secure factory.
     */
    static SchemaFactory secure(final SchemaFactory factory) {
        return new Wrapper(factory);
    }

    private SecureSchemaFactory() {
        // static only
    }
}