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

    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.xpath.XPath;
    025import javax.xml.xpath.XPathFactory;
    026import javax.xml.xpath.XPathFactoryConfigurationException;
    027import javax.xml.xpath.XPathFunctionResolver;
    028import javax.xml.xpath.XPathVariableResolver;
    029
    030/**
    031 * Creates new, secure {@link XPathFactory} instances.
    032 * <p>
    033 * Beyond the three universal guarantees on {@link org.apache.commons.xml.secure}, URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()},
    034 * {@code unparsed-text()}) are not resolved.
    035 * </p>
    036 * <p>
    037 * The guarantees also cover the document parse behind {@code XPath.evaluate(String, InputSource)} and {@code XPathExpression.evaluate(InputSource)}: the input
    038 * document is built through a secure, namespace-aware {@link javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser.
    039 * </p>
    040 * <p>
    041 * This class is not itself a {@link XPathFactory}, so it inherits none of the static JAXP factory methods. A caller therefore cannot obtain an unsecured
    042 * factory through this class by calling a method such as {@code newDefaultInstance()}. The secure factories are instances of a nested, non-public wrapper
    043 * class.
    044 * </p>
    045 *
    046 * @see org.apache.commons.xml.secure
    047 */
    048public final class SecureXPathFactory {
    049
    050    /**
    051     * {@link XPathFactory} wrapper that returns a {@link SecureXPath} from {@link #newXPath()}.
    052     * <p>
    053     * Required because {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} on the factory governs only the XPath engine: the stock JDK and Apache Xalan
    054     * implement the {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points by provisioning an internal document parser the feature does not
    055     * reach. The wrapper performs that document build itself through a secure parser instead; see {@link SecureXPath}.
    056     * </p>
    057     */
    058    private static final class Wrapper extends XPathFactory {
    059
    060        private final XPathFactory delegate;
    061
    062        /**
    063         * Constructs a new instance.
    064         *
    065         * @param delegate The delegate to wrap; must not be {@code null}.
    066         * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
    067         */
    068        private Wrapper(final XPathFactory delegate) {
    069            this.delegate = Objects.requireNonNull(delegate, "delegate");
    070        }
    071
    072        @Override
    073        public boolean getFeature(final String name) throws XPathFactoryConfigurationException {
    074            return delegate.getFeature(name);
    075        }
    076
    077        /**
    078         * Reports a property of the delegate, the Java 18 {@code XPathFactory.getProperty(String)}.
    079         * <p>
    080         * Not marked {@code @Override}: this library compiles against the Java 8 API, where {@link XPathFactory} declares no such method, so the annotation
    081         * would not compile. At run time on Java 18 or later it overrides the inherited method, which would otherwise answer for the wrapper and hide the
    082         * delegate's own limits ({@code jdk.xml.xpath*}) behind an {@code UnsupportedOperationException}.
    083         * </p>
    084         *
    085         * @param name The property name.
    086         * @return the delegate's value for the property.
    087         */
    088        public String getProperty(final String name) {
    089            if (MH_getProperty == null) {
    090                throw new UnsupportedOperationException("XPathFactory.getProperty(String) requires Java 18 or later");
    091            }
    092            return MethodHandleFactory.invokeExact(() -> (String) MH_getProperty.invokeExact(delegate, name), RuntimeException.class);
    093        }
    094
    095        @Override
    096        public boolean isObjectModelSupported(final String objectModel) {
    097            return delegate.isObjectModelSupported(objectModel);
    098        }
    099
    100        @Override
    101        public XPath newXPath() {
    102            // newXPath() should never return null for a specification-compliant factory.
    103            final XPath xpath = delegate.newXPath();
    104            return xpath == null ? null : new SecureXPath(xpath, overrideDefaultParser());
    105        }
    106
    107        /**
    108         * Tests whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}.
    109         * <p>
    110         * The JDK implementation of {@link XPathFactory} uses the JDK parsers while {@value SecureSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or
    111         * {@code false}.
    112         * </p>
    113         *
    114         * @return {@code true} if parsers should be created via {@code newInstance()}.
    115         */
    116        private boolean overrideDefaultParser() {
    117            try {
    118                return delegate.getFeature(SecureSAXParserFactory.OVERRIDE_DEFAULT_PARSER);
    119            } catch (final XPathFactoryConfigurationException e) {
    120                return true;
    121            }
    122        }
    123
    124        @Override
    125        public void setFeature(final String name, final boolean value) throws XPathFactoryConfigurationException {
    126            delegate.setFeature(name, value);
    127        }
    128
    129        /**
    130         * Sets a property on the delegate, the Java 18 {@code XPathFactory.setProperty(String, String)}; see {@link #getProperty(String)} for why it carries no
    131         * {@code @Override}. The {@code jdk.xml.xpath*} limits reached this way are processing limits like any other: an operator may tighten them, and
    132         * loosening one is reconfiguration.
    133         *
    134         * @param name  The property name.
    135         * @param value The value to set.
    136         */
    137        public void setProperty(final String name, final String value) {
    138            if (MH_setProperty == null) {
    139                throw new UnsupportedOperationException("XPathFactory.setProperty(String, String) requires Java 18 or later");
    140            }
    141            MethodHandleFactory.invokeExact(() -> {
    142                MH_setProperty.invokeExact(delegate, name, value);
    143                return null;
    144            }, RuntimeException.class);
    145        }
    146
    147        @Override
    148        public void setXPathFunctionResolver(final XPathFunctionResolver resolver) {
    149            delegate.setXPathFunctionResolver(resolver);
    150        }
    151
    152        @Override
    153        public void setXPathVariableResolver(final XPathVariableResolver resolver) {
    154            delegate.setXPathVariableResolver(resolver);
    155        }
    156    }
    157
    158    /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
    159    private static final String JDK_XPATH_FACTORY = "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl";
    160
    161    private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(XPathFactory.class, "newDefaultInstance");
    162
    163    /** {@code XPathFactory.getProperty(String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist. */
    164    private static final MethodHandle MH_getProperty = MethodHandleFactory.findVirtual(XPathFactory.class, "getProperty", String.class, String.class);
    165
    166    /** {@code XPathFactory.setProperty(String, String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist. */
    167    private static final MethodHandle MH_setProperty =
    168            MethodHandleFactory.findVirtual(XPathFactory.class, "setProperty", void.class, String.class, String.class);
    169
    170    /**
    171     * Returns a new, secure {@link XPathFactory} of the system-default implementation, supporting the default XPath object model.
    172     * <p>
    173     * Obtained from {@code XPathFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in
    174     * implementation directly on Java 8.
    175     * </p>
    176     *
    177     * @return A secure factory.
    178     * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation.
    179     * @throws RuntimeException      Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for
    180     *                               example Android).
    181     */
    182    public static XPathFactory newDefaultInstance() {
    183        if (MH_newDefaultInstance != null) {
    184            return secure(MethodHandleFactory.invokeExact(() -> (XPathFactory) MH_newDefaultInstance.invokeExact(), RuntimeException.class));
    185        }
    186        try {
    187            // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead.
    188            return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, JDK_XPATH_FACTORY, null);
    189        } catch (final XPathFactoryConfigurationException e) {
    190            // newDefaultInstance declares no checked exception; mirror XPathFactory.newInstance(), which reports a default-model miss as a RuntimeException.
    191            throw new RuntimeException("Neither XPathFactory.newDefaultInstance() nor " + JDK_XPATH_FACTORY + " is available", e);
    192        }
    193    }
    194
    195    /**
    196     * Returns a new, secure {@link XPathFactory} for the default XPath object model.
    197     *
    198     * @return A secure factory.
    199     * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation.
    200     * @throws RuntimeException      Thrown if there is a failure in creating an {@link XPathFactory} for the default object model.
    201     */
    202    public static XPathFactory newInstance() {
    203        return secure(XPathFactory.newInstance());
    204    }
    205
    206    /**
    207     * Returns a new, secure {@link XPathFactory} for the given object model.
    208     *
    209     * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}.
    210     * @return A secure factory.
    211     * @throws IllegalStateException              Thrown if a required secure setting cannot be applied to the underlying implementation.
    212     * @throws XPathFactoryConfigurationException Thrown if no implementation of the object model is available.
    213     * @throws NullPointerException               Thrown if {@code uri} is {@code null}.
    214     * @throws IllegalArgumentException           Thrown if {@code uri} is empty.
    215     */
    216    public static XPathFactory newInstance(final String uri) throws XPathFactoryConfigurationException {
    217        return secure(XPathFactory.newInstance(uri));
    218    }
    219
    220    /**
    221     * Returns a new, secure {@link XPathFactory} of the given implementation class.
    222     *
    223     * @param uri              The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}.
    224     * @param factoryClassName The fully qualified class name of the {@link XPathFactory} implementation.
    225     * @param classLoader      The class loader used to load the factory class; {@code null} means the current thread's context class loader.
    226     * @return A secure factory.
    227     * @throws IllegalStateException              Thrown if a required secure setting cannot be applied to the underlying implementation.
    228     * @throws XPathFactoryConfigurationException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or
    229     *                                            instantiated, or does not support {@code uri}.
    230     * @throws NullPointerException               Thrown if {@code uri} is {@code null}.
    231     * @throws IllegalArgumentException           Thrown if {@code uri} is empty.
    232     */
    233    public static XPathFactory newInstance(final String uri, final String factoryClassName, final ClassLoader classLoader)
    234            throws XPathFactoryConfigurationException {
    235        return secure(XPathFactory.newInstance(uri, factoryClassName, classLoader));
    236    }
    237
    238    /**
    239     * Capability-driven securing for any {@link XPathFactory} on the classpath.
    240     *
    241     * <p>The XPath object model mirrors TrAX: the stock JDK and Apache Xalan ship an XPath 1.0 engine with no URI-fetching functions, while Saxon adds the XPath 3.1
    242     * {@code fn:doc}, {@code fn:collection} and {@code fn:unparsed-text} functions that can reach external resources. Rather than branching on the implementation
    243     * class, this method probes what the factory supports and adapts:</p>
    244     * <ul>
    245     *     <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so any public
    246     *         subclass routes to the same recipe as the registered factory. Its URI-fetching
    247     *         functions and reflection-based extension calls are reachable only through a locked-down Saxon {@code Configuration}, not the standard JAXP knobs; this
    248     *         is the XPath counterpart of the Saxon exception in {@link SecureTransformerFactory#secure(javax.xml.transform.TransformerFactory)}, kept as a
    249     *         documented package-prefix exception because the required securing surface is reachable only through a vendor API.</li>
    250     *     <li><strong>FSP</strong> ({@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only knob both the stock JDK and Xalan XPath
    251     *         engines expose, and switches on their secure-processing limits. {@link XPathFactory} has no attribute API for finer control.</li>
    252     *     <li><strong>The nested wrapper</strong>: required. FSP governs only the engine, not the parser it provisions internally for the
    253     *         {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points; the wrapper performs that document build with a secure parser instead, so
    254     *         the engine never parses.</li>
    255     * </ul>
    256     *
    257     * @param factory The factory to secure.
    258     * @return A new secure factory or the original factory, as-is, if it is a known Saxon factory.
    259     * @throws SecureException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature.
    260     */
    261    static XPathFactory secure(final XPathFactory factory) {
    262        if (SaxonProvider.isSaxon(factory.getClass())) {
    263            // Saxon: only a locked-down Configuration can close its URI-fetching functions and extension-function surface.
    264            return SaxonProvider.configure(factory);
    265        }
    266        // Required: enables the engine's secure-processing limits; XPathFactory has no attribute API for finer control.
    267        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
    268        // Required: FSP does not reach the parser the engine provisions for InputSource-taking evaluate calls; the wrapper parses those itself.
    269        return new Wrapper(factory);
    270    }
    271
    272    /**
    273     * Sets a feature on the given factory, throwing a {@link SecureException} if the implementation does not recognize it.
    274     *
    275     * @param factory The factory to secure.
    276     * @param feature The feature to set.
    277     * @param value   The value to set.
    278     * @throws SecureException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature or if {@code feature} is
    279     *                            {@code null}.
    280     */
    281    private static void setFeature(final XPathFactory factory, final String feature, final boolean value) {
    282        try {
    283            factory.setFeature(feature, value);
    284        } catch (final XPathFactoryConfigurationException e) {
    285            throw SecureException.featureFailed(feature, factory, e);
    286        }
    287    }
    288
    289    private SecureXPathFactory() {
    290        // static only
    291    }
    292}