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 an {@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 * Gets a property of the delegate through the Java 18 {@code XPathFactory.getProperty(String)} method. 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 through the Java 18 {@code XPathFactory.setProperty(String, String)} method. 131 * 132 * <p> 133 * See {@link #getProperty(String)} for why it carries no {@code @Override}. The {@code jdk.xml.xpath*} limits reached this way are processing limits 134 * like any other: an operator may tighten them, and loosening one is reconfiguration. 135 * </p> 136 * 137 * @param name The property name. 138 * @param value The value to set. 139 */ 140 public void setProperty(final String name, final String value) { 141 if (MH_setProperty == null) { 142 throw new UnsupportedOperationException("XPathFactory.setProperty(String, String) requires Java 18 or later"); 143 } 144 MethodHandleFactory.invokeExact(() -> { 145 MH_setProperty.invokeExact(delegate, name, value); 146 return null; 147 }, RuntimeException.class); 148 } 149 150 @Override 151 public void setXPathFunctionResolver(final XPathFunctionResolver resolver) { 152 delegate.setXPathFunctionResolver(resolver); 153 } 154 155 @Override 156 public void setXPathVariableResolver(final XPathVariableResolver resolver) { 157 delegate.setXPathVariableResolver(resolver); 158 } 159 } 160 161 /** 162 * Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. 163 */ 164 private static final String JDK_XPATH_FACTORY = "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl"; 165 166 private static final MethodHandle MH_newDefaultInstance = MethodHandleFactory.findStatic(XPathFactory.class, "newDefaultInstance"); 167 168 /** 169 * {@code XPathFactory.getProperty(String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist. 170 */ 171 private static final MethodHandle MH_getProperty = MethodHandleFactory.findVirtual(XPathFactory.class, "getProperty", String.class, String.class); 172 173 /** 174 * {@code XPathFactory.setProperty(String, String)}, added in Java 18; {@code null} on earlier releases, where the method does not exist. 175 */ 176 private static final MethodHandle MH_setProperty = 177 MethodHandleFactory.findVirtual(XPathFactory.class, "setProperty", void.class, String.class, String.class); 178 179 /** 180 * Returns a new, secure {@link XPathFactory} of the system-default implementation, supporting the default XPath object model. 181 * <p> 182 * Obtained from {@code XPathFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in 183 * implementation directly on Java 8. 184 * </p> 185 * 186 * @return A secure factory. 187 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 188 * @throws RuntimeException Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for 189 * example Android). 190 */ 191 public static XPathFactory newDefaultInstance() { 192 if (MH_newDefaultInstance != null) { 193 return secure(MethodHandleFactory.invokeExact(() -> (XPathFactory) MH_newDefaultInstance.invokeExact(), RuntimeException.class)); 194 } 195 try { 196 // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. 197 return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, JDK_XPATH_FACTORY, null); 198 } catch (final XPathFactoryConfigurationException e) { 199 // newDefaultInstance declares no checked exception; mirror XPathFactory.newInstance(), which reports a default-model miss as a RuntimeException. 200 throw new RuntimeException("Neither XPathFactory.newDefaultInstance() nor " + JDK_XPATH_FACTORY + " is available", e); 201 } 202 } 203 204 /** 205 * Returns a new, secure {@link XPathFactory} for the default XPath object model. 206 * 207 * @return A secure factory. 208 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 209 * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. 210 */ 211 public static XPathFactory newInstance() { 212 return secure(XPathFactory.newInstance()); 213 } 214 215 /** 216 * Returns a new, secure {@link XPathFactory} for the given object model. 217 * 218 * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. 219 * @return A secure factory. 220 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 221 * @throws XPathFactoryConfigurationException Thrown if no implementation of the object model is available. 222 * @throws NullPointerException Thrown if {@code uri} is {@code null}. 223 * @throws IllegalArgumentException Thrown if {@code uri} is empty. 224 */ 225 public static XPathFactory newInstance(final String uri) throws XPathFactoryConfigurationException { 226 return secure(XPathFactory.newInstance(uri)); 227 } 228 229 /** 230 * Returns a new, secure {@link XPathFactory} of the given implementation class. 231 * 232 * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. 233 * @param factoryClassName The fully qualified class name of the {@link XPathFactory} implementation. 234 * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. 235 * @return A secure factory. 236 * @throws IllegalStateException Thrown if a required secure setting cannot be applied to the underlying implementation. 237 * @throws XPathFactoryConfigurationException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or 238 * instantiated, or does not support {@code uri}. 239 * @throws NullPointerException Thrown if {@code uri} is {@code null}. 240 * @throws IllegalArgumentException Thrown if {@code uri} is empty. 241 */ 242 public static XPathFactory newInstance(final String uri, final String factoryClassName, final ClassLoader classLoader) 243 throws XPathFactoryConfigurationException { 244 return secure(XPathFactory.newInstance(uri, factoryClassName, classLoader)); 245 } 246 247 /** 248 * Applies capability-driven secure settings to any {@link XPathFactory} on the classpath. 249 * 250 * <p> 251 * 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 252 * 3.1 253 * {@code fn:doc}, {@code fn:collection} and {@code fn:unparsed-text} functions that can reach external resources. Rather than branching on the 254 * implementation class, this method probes what the factory supports and adapts: 255 * </p> 256 * <ul> 257 * <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so any 258 * public subclass routes to the same recipe as the registered factory. Its URI-fetching 259 * functions and reflection-based extension calls are reachable only through a locked-down Saxon {@code Configuration}, not the standard JAXP knobs; this 260 * is the XPath counterpart of the Saxon exception in {@link SecureTransformerFactory#secure(javax.xml.transform.TransformerFactory)}, kept as a 261 * documented package-prefix exception because the required securing surface is reachable only through a vendor API.</li> 262 * <li><strong>FSP</strong> ({@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only knob both the stock JDK and Xalan XPath 263 * engines expose, and switches on their secure-processing limits. {@link XPathFactory} has no attribute API for finer control.</li> 264 * <li><strong>The nested wrapper</strong>: required. FSP governs only the engine, not the parser it provisions internally for the 265 * {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points; the wrapper performs that document build with a secure parser instead, so 266 * the engine never parses.</li> 267 * </ul> 268 * 269 * @param factory The factory to secure. 270 * @return A new secure factory or the original factory, as-is, if it is a known Saxon factory. 271 * @throws SecureException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature. 272 */ 273 static XPathFactory secure(final XPathFactory factory) { 274 if (SaxonProvider.isSaxon(factory.getClass())) { 275 // Saxon: only a locked-down Configuration can close its URI-fetching functions and extension-function surface. 276 return SaxonProvider.configure(factory); 277 } 278 // Required: enables the engine's secure-processing limits; XPathFactory has no attribute API for finer control. 279 setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); 280 // Required: FSP does not reach the parser the engine provisions for InputSource-taking evaluate calls; the wrapper parses those itself. 281 return new Wrapper(factory); 282 } 283 284 /** 285 * Sets a feature on the given factory, throwing a {@link SecureException} if the implementation does not recognize it. 286 * 287 * @param factory The factory to secure. 288 * @param feature The feature to set. 289 * @param value The value to set. 290 * @throws SecureException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature or if {@code feature} is 291 * {@code null}. 292 */ 293 private static void setFeature(final XPathFactory factory, final String feature, final boolean value) { 294 try { 295 factory.setFeature(feature, value); 296 } catch (final XPathFactoryConfigurationException e) { 297 throw SecureException.featureFailed(feature, factory, e); 298 } 299 } 300 301 private SecureXPathFactory() { 302 // static only 303 } 304}