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

    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.DocumentBuilder;
    025import javax.xml.parsers.DocumentBuilderFactory;
    026import javax.xml.parsers.FactoryConfigurationError;
    027import javax.xml.parsers.ParserConfigurationException;
    028import javax.xml.validation.Schema;
    029
    030import org.xml.sax.EntityResolver;
    031
    032/**
    033 * Creates new, secure {@link DocumentBuilderFactory} instances.
    034 * <p>
    035 * Beyond the three universal guarantees on {@link org.apache.commons.xml.secure}, XInclude resolution is denied by default. When
    036 * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process {@code xi:include}
    037 * elements but every external resource lookup is rejected. To permit specific trusted resources, install an {@link org.xml.sax.EntityResolver EntityResolver}
    038 * on the {@link DocumentBuilder} that allow-lists them; any href the resolver does not explicitly allow stays blocked.
    039 * </p>
    040 * <p>
    041 * This class is not itself a {@link DocumentBuilderFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an
    042 * unsecured factory through this class by calling a method such as {@code newDefaultInstance()}. The secure factories are instances of a nested, non-public
    043 * wrapper class.
    044 * </p>
    045 *
    046 * @see org.apache.commons.xml.secure
    047 */
    048public final class SecureDocumentBuilderFactory {
    049
    050    /**
    051     * {@link DocumentBuilderFactory} wrapper that keeps an ignore-all {@link EntityResolver} floor on every {@link DocumentBuilder} produced.
    052     * <p>
    053     * Wraps each produced builder in a {@link SecureDocumentBuilder}; required when the underlying factory carries no resolver of its own and does not honor
    054     * JAXP 1.5 {@code ACCESS_EXTERNAL_*} (e.g. the external Xerces distribution). A caller-set resolver is routed through the floor rather than replacing it.
    055     * </p>
    056     */
    057    private static final class Wrapper extends DocumentBuilderFactory {
    058
    059        private final DocumentBuilderFactory delegate;
    060
    061        /**
    062         * Constructs a new instance.
    063         *
    064         * @param delegate The delegate to wrap; must not be {@code null}.
    065         * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
    066         */
    067        private Wrapper(final DocumentBuilderFactory delegate) {
    068            this.delegate = Objects.requireNonNull(delegate, "delegate");
    069        }
    070
    071        @Override
    072        public Object getAttribute(final String name) {
    073            return delegate.getAttribute(name);
    074        }
    075
    076        @Override
    077        public boolean getFeature(final String name) throws ParserConfigurationException {
    078            return delegate.getFeature(name);
    079        }
    080
    081        @Override
    082        public Schema getSchema() {
    083            return delegate.getSchema();
    084        }
    085
    086        @Override
    087        public boolean isCoalescing() {
    088            return delegate.isCoalescing();
    089        }
    090
    091        @Override
    092        public boolean isExpandEntityReferences() {
    093            return delegate.isExpandEntityReferences();
    094        }
    095
    096        @Override
    097        public boolean isIgnoringComments() {
    098            return delegate.isIgnoringComments();
    099        }
    100
    101        @Override
    102        public boolean isIgnoringElementContentWhitespace() {
    103            return delegate.isIgnoringElementContentWhitespace();
    104        }
    105
    106        @Override
    107        public boolean isNamespaceAware() {
    108            return delegate.isNamespaceAware();
    109        }
    110
    111        @Override
    112        public boolean isValidating() {
    113            return delegate.isValidating();
    114        }
    115
    116        @Override
    117        public boolean isXIncludeAware() {
    118            return delegate.isXIncludeAware();
    119        }
    120
    121        @Override
    122        public DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
    123            return new SecureDocumentBuilder(delegate.newDocumentBuilder());
    124        }
    125
    126        @Override
    127        public void setAttribute(final String name, final Object value) {
    128            delegate.setAttribute(name, value);
    129        }
    130
    131        @Override
    132        public void setCoalescing(final boolean coalescing) {
    133            delegate.setCoalescing(coalescing);
    134        }
    135
    136        @Override
    137        public void setExpandEntityReferences(final boolean expandEntityRef) {
    138            delegate.setExpandEntityReferences(expandEntityRef);
    139        }
    140
    141        @Override
    142        public void setFeature(final String name, final boolean value) throws ParserConfigurationException {
    143            delegate.setFeature(name, value);
    144        }
    145
    146        @Override
    147        public void setIgnoringComments(final boolean ignoreComments) {
    148            delegate.setIgnoringComments(ignoreComments);
    149        }
    150
    151        @Override
    152        public void setIgnoringElementContentWhitespace(final boolean whitespace) {
    153            delegate.setIgnoringElementContentWhitespace(whitespace);
    154        }
    155
    156        @Override
    157        public void setNamespaceAware(final boolean awareness) {
    158            delegate.setNamespaceAware(awareness);
    159        }
    160
    161        @Override
    162        public void setSchema(final Schema schema) {
    163            delegate.setSchema(schema);
    164        }
    165
    166        @Override
    167        public void setValidating(final boolean validating) {
    168            delegate.setValidating(validating);
    169        }
    170
    171        @Override
    172        public void setXIncludeAware(final boolean state) {
    173            delegate.setXIncludeAware(state);
    174        }
    175    }
    176    /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no secure surface. */
    177    private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl";
    178    /** System property naming the {@link DocumentBuilderFactory} implementation, the JDK's own mechanism for reconfiguring the default parser. */
    179    private static final String DOM_FACTORY_ID = "javax.xml.parsers.DocumentBuilderFactory";
    180
    181    /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
    182    static final String JDK_DOCUMENT_BUILDER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl";
    183
    184    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(DocumentBuilderFactory.class, "newDefaultInstance");
    185
    186    /**
    187     * Enables namespace awareness on the given factory; the {@code NSInstance} counterpart of each factory method routes its result through here.
    188     *
    189     * @param factory The factory to configure; never {@code null}.
    190     * @return The given factory, namespace-aware.
    191     */
    192    private static DocumentBuilderFactory makeNSAware(final DocumentBuilderFactory factory) {
    193        factory.setNamespaceAware(true);
    194        return factory;
    195    }
    196
    197    /**
    198     * Returns a new, secure {@link DocumentBuilderFactory} of the system-default implementation.
    199     * <p>
    200     * Obtained from {@code DocumentBuilderFactory.newDefaultInstance()} where the platform provides it (Java 9 or later),
    201     * by instantiating the JDK's built-in implementation directly on Java 8,
    202     * and by the standard {@link #newInstance()} lookup where the platform provides neither
    203     * (for example, Android, whose lookup is itself pinned to the platform implementation).
    204     * </p>
    205     *
    206     * @return A secure factory.
    207     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    208     * @throws FactoryConfigurationError Thrown from the {@link #newInstance()} lookup this method falls back to on a platform that provides neither
    209     *                                   {@code newDefaultInstance()} nor the JDK's built-in implementation (for example Android).
    210     */
    211    public static DocumentBuilderFactory newDefaultInstance() {
    212        if (MH_newDefaultInstance != null) {
    213            return secure(MethodHandleFactory.invokeExact(() -> (DocumentBuilderFactory) MH_newDefaultInstance.invokeExact(), FactoryConfigurationError.class));
    214        }
    215        try {
    216            // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead.
    217            return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null);
    218        } catch (final FactoryConfigurationError e) {
    219            // Neither exists (for example, Android): degrade to the regular lookup, which such platforms pin to their built-in parser.
    220            return newInstance();
    221        }
    222    }
    223
    224    /**
    225     * Returns a new, secure, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation, enabling namespace awareness on
    226     * {@link #newDefaultInstance()}, the behavior {@code DocumentBuilderFactory.newDefaultNSInstance()} (Java 13 or later) is specified to have.
    227     *
    228     * @return A secure, namespace-aware factory.
    229     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    230     * @throws FactoryConfigurationError Thrown from the {@link #newInstance()} lookup {@link #newDefaultInstance()} falls back to on a platform that provides
    231     *                                   neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for example Android).
    232     */
    233    public static DocumentBuilderFactory newDefaultNSInstance() {
    234        return makeNSAware(newDefaultInstance());
    235    }
    236
    237    /**
    238     * Returns a new, secure {@link DocumentBuilderFactory}.
    239     *
    240     * @return A secure factory.
    241     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    242     * @throws IllegalStateException     Thrown if a (non-Android) factory cannot support the secure processing feature
    243     *                                   {@link XMLConstants#FEATURE_SECURE_PROCESSING}.
    244     * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the
    245     *                                   implementation is not available or cannot be instantiated.
    246     */
    247    public static DocumentBuilderFactory newInstance() {
    248        return secure(DocumentBuilderFactory.newInstance());
    249    }
    250
    251    /**
    252     * Returns a new, secure {@link DocumentBuilderFactory} of the given implementation class.
    253     *
    254     * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation.
    255     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
    256     * @return A secure factory.
    257     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    258     * @throws IllegalStateException     Thrown if a (non-Android) factory cannot support the secure processing feature
    259     *                                   {@link XMLConstants#FEATURE_SECURE_PROCESSING}.
    260     * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated.
    261     */
    262    public static DocumentBuilderFactory newInstance(final String factoryClassName, final ClassLoader classLoader) {
    263        return secure(DocumentBuilderFactory.newInstance(factoryClassName, classLoader));
    264    }
    265
    266    /**
    267     * Returns a new, secure, namespace-aware {@link DocumentBuilderFactory}, enabling namespace awareness on {@link #newInstance()}, the behavior
    268     * {@code DocumentBuilderFactory.newNSInstance()} (Java 13 or later) is specified to have.
    269     *
    270     * @return A secure, namespace-aware factory.
    271     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    272     * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the
    273     *                                   implementation is not available or cannot be instantiated.
    274     */
    275    public static DocumentBuilderFactory newNSInstance() {
    276        return makeNSAware(newInstance());
    277    }
    278
    279    /**
    280     * Returns the secure, namespace-aware factory the Source-rewriting wrappers parse with.
    281     * <p>
    282     * While {@code overrideDefaultParser} is {@code false} the factory is the JDK's "default parser" factory, determined the way the JDK itself determines it: the built-in
    283     * implementation, unless the {@value #DOM_FACTORY_ID} system property is set — that property is the JDK's own mechanism for
    284     * reconfiguring the default parser, so it is honored through the standard lookup rather than bypassed.
    285     * </p>
    286     *
    287     * @param overrideDefaultParser whether {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} on the originating factory asks to override the JDK's default parser.
    288     * @return A secure, namespace-aware factory.
    289     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    290     * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the
    291     *                                   implementation is not available or cannot be instantiated.
    292     */
    293    static DocumentBuilderFactory newNSInstance(final boolean overrideDefaultParser) {
    294        return overrideDefaultParser || System.getProperty(DOM_FACTORY_ID) != null ? newNSInstance() : newDefaultNSInstance();
    295    }
    296
    297    /**
    298     * Returns a new, secure, namespace-aware {@link DocumentBuilderFactory} of the given implementation class, enabling namespace awareness on
    299     * {@link #newInstance(String, ClassLoader)}, the behavior {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified
    300     * to have.
    301     *
    302     * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation.
    303     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
    304     * @return A secure, namespace-aware factory.
    305     * @throws IllegalStateException     Thrown if a required secure setting cannot be applied to the underlying implementation.
    306     * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated.
    307     */
    308    public static DocumentBuilderFactory newNSInstance(final String factoryClassName, final ClassLoader classLoader) {
    309        return makeNSAware(newInstance(factoryClassName, classLoader));
    310    }
    311
    312    /**
    313     * Capability-driven secure for any {@link DocumentBuilderFactory} on the classpath.
    314     *
    315     * <p>Rather than branching on the implementation class, this method probes what the factory supports and adapts:</p>
    316     * <ul>
    317     *     <li><strong>Android</strong> (Harmony / KXmlParser): recognized by class name and left untouched. It exposes no {@link XMLConstants#FEATURE_SECURE_PROCESSING
    318     *         FSP}, no JAXP 1.5 {@code ACCESS_EXTERNAL_*} and no attribute API at all, while KXmlParser silently drops user-defined entities, so there is nothing to
    319     *         apply.</li>
    320     *     <li><strong>FSP</strong>: required. It switches on the implementation's built-in security manager, which is what carries the processing limits.</li>
    321     *     <li><strong>Ignore-all resolver floor</strong>: every produced {@link DocumentBuilder} is wrapped by the nested wrapper, which keeps an
    322     *         ignore-all {@link EntityResolver} floor. That floor blocks external DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's
    323     *         XInclude processor ignores {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} attributes are
    324     *         needed here. A caller can chain its own resolver onto the floor to allow-list resources, but cannot remove it.</li>
    325     * </ul>
    326     *
    327     * @param factory The factory to secure.
    328     * @return A new secure factory or the original factory, as-is, if it is a known Android factory.
    329     * @throws SecureException Thrown if a (non-Android) factory cannot support the secure processing feature {@link XMLConstants#FEATURE_SECURE_PROCESSING}.
    330     */
    331    static DocumentBuilderFactory secure(final DocumentBuilderFactory factory) {
    332        // Android exposes no FSP, ACCESS_EXTERNAL_* or attribute API, and KXmlParser drops user-defined entities; nothing to apply.
    333        if (ANDROID_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
    334            return factory;
    335        }
    336        // Required: enables the implementation's security manager, which carries the limits.
    337        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
    338        // Required: the wrapper installs an ignore-all EntityResolver floor on every DocumentBuilder.
    339        // That floor blocks external DTD, entity, schema and xi:include fetches in one place: no ACCESS_EXTERNAL_* attributes are needed here.
    340        // Callers can chain their resolvers, but not override the floor.
    341        return new Wrapper(factory);
    342    }
    343
    344    /**
    345     * Sets a feature on the given factory, throwing a {@link SecureException} if the implementation does not recognize it.
    346     *
    347     * @param factory The factory to secure.
    348     * @param feature The feature to set.
    349     * @param value   The value to set.
    350     * @throws SecureException   Thrown if this factory cannot support this feature.
    351     * @throws NullPointerException Thrown if the {@code feature} parameter is null.
    352     */
    353    private static void setFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) {
    354        try {
    355            factory.setFeature(feature, value);
    356        } catch (final ParserConfigurationException e) {
    357            throw SecureException.featureFailed(feature, factory, e);
    358        }
    359    }
    360
    361    private SecureDocumentBuilderFactory() {
    362        // static only
    363    }
    364}