Source code
Description: source: package: org.apache.commons.xml.secure, class: SecureSchemaFactory

    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}