+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When + * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process + * {@code xi:include} elements but every external resource lookup is rejected. To permit specific trusted resources, install an + * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link DocumentBuilder} that allow-lists them; any href the resolver does not explicitly allow + * stays blocked. + *
+ ** Not a {@link DocumentBuilderFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -41,6 +51,19 @@ public final class HardeningDocumentBuilderFactory { /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */ private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_DOCUMENT_BUILDER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(DocumentBuilderFactory.class)); + + private static MethodHandle findStatic(final String name, final MethodType type) { + try { + return MethodHandles.publicLookup().findStatic(DocumentBuilderFactory.class, name, type); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link DocumentBuilderFactory} on the classpath. @@ -75,17 +98,64 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { } /** - * Returns a new, hardened {@link DocumentBuilderFactory}. + * Enables namespace awareness on the given factory; the {@code NSInstance} counterpart of each factory method routes its result through here. + * + * @param factory the factory to configure; never {@code null}. + * @return The given factory, namespace-aware. + */ + private static DocumentBuilderFactory makeNSAware(final DocumentBuilderFactory factory) { + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the system-default implementation. *- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When - * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process - * {@code xi:include} elements but every external resource lookup is rejected. To permit specific trusted resources, install an - * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link DocumentBuilder} that allow-lists them; any href the resolver does not explicitly allow - * stays blocked. + * Obtained as by {@code DocumentBuilderFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and + * by instantiating the JDK's built-in implementation directly on Java 8. *
* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static DocumentBuilderFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final DocumentBuilderFactory factory; + try { + factory = (DocumentBuilderFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as the factory's own FactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation, enabling namespace awareness on + * {@link #newDefaultInstance()}, the behavior {@code DocumentBuilderFactory.newDefaultNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static DocumentBuilderFactory newDefaultNSInstance() { + return makeNSAware(newDefaultInstance()); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. * @throws IllegalStateException Thrown if a (non-Andoid) factory cannot support the secure processing feature * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the @@ -95,6 +165,49 @@ public static DocumentBuilderFactory newInstance() { return harden(DocumentBuilderFactory.newInstance()); } + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalStateException Thrown if a (non-Andoid) factory cannot support the secure processing feature + * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static DocumentBuilderFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory}, enabling namespace awareness on {@link #newInstance()}, the behavior + * {@code DocumentBuilderFactory.newNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @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. + */ + public static DocumentBuilderFactory newNSInstance() { + return makeNSAware(newInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class, enabling namespace awareness on + * {@link #newInstance(String, ClassLoader)}, the behavior {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified + * to have. + * + * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static DocumentBuilderFactory newNSInstance(final String factoryClassName, final ClassLoader classLoader) { + return makeNSAware(newInstance(factoryClassName, classLoader)); + } + /** * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. * diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index 27d91dbc..9101acd1 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -17,6 +17,9 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import javax.xml.XMLConstants; @@ -40,6 +43,12 @@ /** * Creates new, hardened {@link SAXParserFactory} instances. *+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When + * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process {@code xi:include} + * elements but every external resource lookup is rejected. To permit specific trusted resources, install an {@link org.xml.sax.EntityResolver + * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists them; any href the resolver does not explicitly allow stays blocked. + *
+ ** Not a {@link SAXParserFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -52,6 +61,19 @@ public final class HardeningSAXParserFactory { private static final String ANDROID_EXPAT_READER = "org.apache.harmony.xml.ExpatReader"; /** Class name of Android's Harmony-based {@link SAXParserFactory}, backed by the native Expat parser. */ private static final String ANDROID_SAX_PARSER_FACTORY = "org.apache.harmony.xml.parsers.SAXParserFactoryImpl"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_SAX_PARSER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(SAXParserFactory.class)); + + private static MethodHandle findStatic(final String name, final MethodType type) { + try { + return MethodHandles.publicLookup().findStatic(SAXParserFactory.class, name, type); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link SAXParserFactory} on the classpath. @@ -132,6 +154,60 @@ static XMLReader harden(final XMLReader reader) { return new HardeningXMLReader(reader); } + /** + * Enables namespace awareness on the given factory; the {@code NSInstance} counterpart of each factory method routes its result through here. + * + * @param factory the factory to configure; never {@code null}. + * @return The given factory, namespace-aware. + */ + private static SAXParserFactory makeNSAware(final SAXParserFactory factory) { + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the system-default implementation. + *+ * Obtained as by {@code SAXParserFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by + * instantiating the JDK's built-in implementation directly on Java 8. + *
+ * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SAXParserFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final SAXParserFactory factory; + try { + factory = (SAXParserFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as the factory's own FactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_SAX_PARSER_FACTORY, null); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the system-default implementation, enabling namespace awareness on + * {@link #newDefaultInstance()}, the behavior {@code SAXParserFactory.newDefaultNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SAXParserFactory newDefaultNSInstance() { + return makeNSAware(newDefaultInstance()); + } + /** * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. * @@ -142,9 +218,7 @@ static XMLReader harden(final XMLReader reader) { */ static XMLReader newHardenedReader() throws TransformerConfigurationException { try { - final SAXParserFactory factory = harden(SAXParserFactory.newInstance()); - factory.setNamespaceAware(true); - return factory.newSAXParser().getXMLReader(); + return newNSInstance().newSAXParser().getXMLReader(); } catch (final ParserConfigurationException | SAXException e) { throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e); } @@ -152,12 +226,6 @@ static XMLReader newHardenedReader() throws TransformerConfigurationException { /** * Returns a new, hardened {@link SAXParserFactory}. - *- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When - * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process {@code xi:include} - * elements but every external resource lookup is rejected. To permit specific trusted resources, install an {@link org.xml.sax.EntityResolver - * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists them; any href the resolver does not explicitly allow stays blocked. - *
* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. @@ -168,6 +236,46 @@ public static SAXParserFactory newInstance() { return harden(SAXParserFactory.newInstance()); } + /** + * Returns a new, hardened {@link SAXParserFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link SAXParserFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static SAXParserFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory}, enabling namespace awareness on {@link #newInstance()}, the behavior + * {@code SAXParserFactory.newNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown from {@link SAXParserFactory} in case of a {@link java.util.ServiceConfigurationError service configuration + * error} or if the implementation is not available or cannot be instantiated. + */ + public static SAXParserFactory newNSInstance() { + return makeNSAware(newInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class, enabling namespace awareness on + * {@link #newInstance(String, ClassLoader)}, the behavior {@code SAXParserFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified to have. + * + * @param factoryClassName The fully qualified class name of the {@link SAXParserFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static SAXParserFactory newNSInstance(final String factoryClassName, final ClassLoader classLoader) { + return makeNSAware(newInstance(factoryClassName, classLoader)); + } + private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index a123b4a1..539b8cfa 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -17,8 +17,12 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +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.transform.TransformerConfigurationException; @@ -36,6 +40,17 @@ /** * Creates new, hardened {@link SchemaFactory} instances. *+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}: + *
+ *+ * 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}. + *
+ ** Not a {@link SchemaFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -44,6 +59,21 @@ */ public final class HardeningSchemaFactory { + /** 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 NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(SchemaFactory.class, "newDefaultInstance", + MethodType.methodType(SchemaFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } + /** * Hardening for any {@link SchemaFactory} on the classpath. * @@ -60,19 +90,38 @@ static SchemaFactory harden(final SchemaFactory factory) { } /** - * Returns a new, hardened {@link SchemaFactory} for the given schema language. + * Returns a new, hardened {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0. *- * Beyond the three universal guarantees on {@link org.apache.commons.xml}: - *
- *- * 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}. + * Obtained as by {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *
* + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalArgumentException Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SchemaFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final SchemaFactory factory; + try { + factory = (SchemaFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final SchemaFactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as IllegalArgumentException, the error SchemaFactory.newInstance(String, String, ClassLoader) defines. + return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null); + } + + /** + * Returns a new, hardened {@link SchemaFactory} for the given schema language. + * * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. * @return A hardened factory. * @throws IllegalArgumentException Thrown if no implementation of the schema language is available. @@ -83,6 +132,21 @@ public static SchemaFactory newInstance(final String schemaLanguage) { return harden(SchemaFactory.newInstance(schemaLanguage)); } + /** + * Returns a new, hardened {@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 hardened 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 harden(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); + } + private HardeningSchemaFactory() { // static only } diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index 172c9880..7f030c56 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -18,6 +18,9 @@ package org.apache.commons.xml; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import java.util.function.Supplier; @@ -31,6 +34,7 @@ import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.URIResolver; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; @@ -48,6 +52,23 @@ /** * Creates new, hardened {@link TransformerFactory} instances. *+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. + *
+ *+ * The guarantees govern what the transform reads, not what it writes: an output instruction like {@code xsl:result-document} still writes wherever the + * stylesheet directs, so an untrusted stylesheet's output destinations must be restricted outside the library. + *
+ *+ * The guarantees apply to every parser the factory creates internally for the standard {@link TransformerFactory} entry points: stylesheet compilation + * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) newTemplates(Source)}, + * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) newTransformer(Source)}) and source-document reading at + * {@code Transformer.transform(Source, Result)} time. + *
+ *+ * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()}, + * {@code newXMLFilter(..)}), if reachable by casting the returned factory, produce objects carrying the same guarantees. + *
+ ** Not a {@link TransformerFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -56,6 +77,21 @@ */ public final class HardeningTransformerFactory { + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_TRANSFORMER_FACTORY = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(TransformerFactory.class, "newDefaultInstance", + MethodType.methodType(TransformerFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } + /** * Capability-driven hardening for any {@link TransformerFactory} on the classpath. * @@ -94,32 +130,58 @@ static TransformerFactory harden(final TransformerFactory factory) { } /** - * Returns a new, hardened {@link TransformerFactory}. - *- * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. - *
- *- * The guarantees govern what the transform reads, not what it writes: an output instruction like {@code xsl:result-document} still writes wherever the - * stylesheet directs, so an untrusted stylesheet's output destinations must be restricted outside the library. - *
- *- * The guarantees apply to every parser the factory creates internally for the standard {@link TransformerFactory} entry points: stylesheet compilation - * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) newTemplates(Source)}, - * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) newTransformer(Source)}) and source-document reading at - * {@code Transformer.transform(Source, Result)} time. - *
+ * Returns a new, hardened {@link TransformerFactory} of the system-default implementation. *- * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()}, - * {@code newXMLFilter(..)}), if reachable by casting the returned factory, produce objects carrying the same guarantees. + * Obtained as by {@code TransformerFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *
* * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in + * implementation (for example Android). + */ + public static TransformerFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final TransformerFactory factory; + try { + factory = (TransformerFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final TransformerFactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as TransformerFactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_TRANSFORMER_FACTORY, null); + } + + /** + * Returns a new, hardened {@link TransformerFactory}. + * + * @return A hardened factory. * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. */ public static TransformerFactory newInstance() { return harden(TransformerFactory.newInstance()); } + /** + * Returns a new, hardened {@link TransformerFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link TransformerFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static TransformerFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(TransformerFactory.newInstance(factoryClassName, classLoader)); + } + private static void setFeature(final TransformerFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); @@ -182,8 +244,7 @@ private static Source hardenSourceToDom(final Source source) throws TransformerC final InputSource inputSource = SAXSource.sourceToInputSource(source); if (inputSource != null) { try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); - factory.setNamespaceAware(true); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); final Document document = factory.newDocumentBuilder().parse(inputSource); return new DOMSource(document, inputSource.getSystemId()); } catch (final ParserConfigurationException | SAXException | IOException e) { diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java index 37ed3932..6f155423 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java @@ -19,10 +19,13 @@ import java.io.InputStream; import java.io.Reader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; -import javax.xml.parsers.FactoryConfigurationError; import javax.xml.stream.EventFilter; +import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.StreamFilter; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; @@ -36,6 +39,9 @@ /** * Creates new, hardened {@link XMLInputFactory} instances. *+ * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. + *
+ ** Not a {@link XMLInputFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultFactory()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -50,6 +56,20 @@ public final class HardeningXMLInputFactory { static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver"; /** Woodstox property: resolver consulted for undeclared entity references. */ static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultFactory()}. */ + private static final String JDK_XML_INPUT_FACTORY = "com.sun.xml.internal.stream.XMLInputFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_FACTORY = findNewDefaultFactory(); + + private static MethodHandle findNewDefaultFactory() { + try { + return MethodHandles.publicLookup().findStatic(XMLInputFactory.class, "newDefaultFactory", + MethodType.methodType(XMLInputFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath. @@ -67,13 +87,70 @@ static XMLInputFactory harden(final XMLInputFactory factory) { } /** - * Returns a new, hardened {@link XMLInputFactory}. + * Returns a new, hardened {@link XMLInputFactory} of the system-default implementation. *- * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. + * Obtained as by {@code XMLInputFactory.newDefaultFactory()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *
* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultFactory()} nor the JDK's built-in implementation + * (for example Android). + */ + public static XMLInputFactory newDefaultFactory() { + if (NEW_DEFAULT_FACTORY != null) { + final XMLInputFactory factory; + try { + factory = (XMLInputFactory) NEW_DEFAULT_FACTORY.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + try { + // Java 8: the method does not exist, and XMLInputFactory has no class-name-taking lookup; instantiate the JDK's built-in default directly. + return harden((XMLInputFactory) Class.forName(JDK_XML_INPUT_FACTORY).getConstructor().newInstance()); + } catch (final ReflectiveOperationException e) { + // Where the class does not exist either (for example Android), report the miss like any StAX factory lookup: with FactoryConfigurationError. + throw new FactoryConfigurationError(e, "Neither XMLInputFactory.newDefaultFactory() nor " + JDK_XML_INPUT_FACTORY + " is available"); + } + } + + /** + * Returns a new, hardened {@link XMLInputFactory}, as by {@link XMLInputFactory#newFactory()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. + */ + public static XMLInputFactory newFactory() { + // XMLInputFactory.newInstance, not newFactory: the same specified lookup, but Android's StAX API predates newFactory. + return harden(XMLInputFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XMLInputFactory} resolved from the given factory id. + * + * @param factoryId The name of the factory to find; a system property or service id to look up, not the class name of the implementation. + * @param classLoader The class loader used in the lookup; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown in case of a service configuration error or if the implementation is not available or cannot be instantiated. + * @throws NullPointerException Thrown if {@code factoryId} is {@code null}. + */ + public static XMLInputFactory newFactory(final String factoryId, final ClassLoader classLoader) { + return harden(XMLInputFactory.newFactory(factoryId, classLoader)); + } + + /** + * Returns a new, hardened {@link XMLInputFactory}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. */ public static XMLInputFactory newInstance() { diff --git a/src/main/java/org/apache/commons/xml/HardeningXPath.java b/src/main/java/org/apache/commons/xml/HardeningXPath.java index abac0b73..c843fea6 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPath.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPath.java @@ -66,8 +66,7 @@ final class HardeningXPath implements XPath { static Document parse(final InputSource source) throws XPathExpressionException { Objects.requireNonNull(source, "source"); try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); - factory.setNamespaceAware(true); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); return factory.newDocumentBuilder().parse(source); } catch (final ParserConfigurationException | SAXException | IOException e) { throw new XPathExpressionException(e); diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index eb33fafc..cde63abb 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java @@ -17,6 +17,9 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import javax.xml.XMLConstants; @@ -29,6 +32,14 @@ /** * Creates new, hardened {@link XPathFactory} instances. *+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()}, + * {@code unparsed-text()}) are not resolved. + *
+ *+ * The guarantees also cover the document parse behind {@code XPath.evaluate(String, InputSource)} and {@code XPathExpression.evaluate(InputSource)}: the + * input document is built through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser. + *
+ ** Not a {@link XPathFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *
@@ -37,23 +48,19 @@ */ public final class HardeningXPathFactory { - /** - * Returns a new, hardened {@link XPathFactory} for the default XPath object model. - *- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()}, - * {@code unparsed-text()}) are not resolved. - *
- *- * The guarantees also cover the document parse behind {@code XPath.evaluate(String, InputSource)} and {@code XPathExpression.evaluate(InputSource)}: the - * input document is built through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser. - *
- * - * @return A hardened factory. - * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. - * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. - */ - public static XPathFactory newInstance() { - return harden(XPathFactory.newInstance()); + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_XPATH_FACTORY = "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(XPathFactory.class, "newDefaultInstance", + MethodType.methodType(XPathFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } } /** @@ -94,6 +101,84 @@ static XPathFactory harden(final XPathFactory factory) { return new Wrapper(factory); } + /** + * Returns a new, hardened {@link XPathFactory} of the system-default implementation, supporting the default XPath object model. + *+ * Obtained as by {@code XPathFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. + *
+ * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws RuntimeException Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for + * example Android). + */ + public static XPathFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final XPathFactory factory; + try { + factory = (XPathFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + try { + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. + return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, JDK_XPATH_FACTORY, null); + } catch (final XPathFactoryConfigurationException e) { + // newDefaultInstance declares no checked exception; mirror XPathFactory.newInstance(), which reports a default-model miss as a RuntimeException. + throw new RuntimeException( + "Neither XPathFactory.newDefaultInstance() nor " + JDK_XPATH_FACTORY + " is available", e); + } + } + + /** + * Returns a new, hardened {@link XPathFactory} for the default XPath object model. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. + */ + public static XPathFactory newInstance() { + return harden(XPathFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XPathFactory} for the given object model. + * + * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws XPathFactoryConfigurationException Thrown if no implementation of the object model is available. + * @throws NullPointerException Thrown if {@code uri} is {@code null}. + * @throws IllegalArgumentException Thrown if {@code uri} is empty. + */ + public static XPathFactory newInstance(final String uri) throws XPathFactoryConfigurationException { + return harden(XPathFactory.newInstance(uri)); + } + + /** + * Returns a new, hardened {@link XPathFactory} of the given implementation class. + * + * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. + * @param factoryClassName The fully qualified class name of the {@link XPathFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws XPathFactoryConfigurationException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or + * instantiated, or does not support {@code uri}. + * @throws NullPointerException Thrown if {@code uri} is {@code null}. + * @throws IllegalArgumentException Thrown if {@code uri} is empty. + */ + public static XPathFactory newInstance(final String uri, final String factoryClassName, final ClassLoader classLoader) + throws XPathFactoryConfigurationException { + return harden(XPathFactory.newInstance(uri, factoryClassName, classLoader)); + } + /** * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. * diff --git a/src/main/java/org/apache/commons/xml/package-info.java b/src/main/java/org/apache/commons/xml/package-info.java index c9e3f7a9..c837c8c1 100644 --- a/src/main/java/org/apache/commons/xml/package-info.java +++ b/src/main/java/org/apache/commons/xml/package-info.java @@ -33,7 +33,7 @@ * ** These guarantees are defined on OpenJDK 8 or later (and JDK distributions built from it). No version of Android supports - * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android (API level 19 or later) the hardening is applied as best-effort without a guarantee, + * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android (API level 26 or later) the hardening is applied as best-effort without a guarantee, * tested as complete starting with API level 33; see the threat model's "Assumptions about the environment". *
*@@ -46,6 +46,19 @@ * Each method adds factory-specific guarantees on top of the three above, documented on the corresponding {@code newXxxFactory()} method. *
*+ * Each factory class mirrors every static factory method of its JAXP counterpart: + *
+ *+ * All of them work, with the same semantics, on every supported runtime, including Java 8. The {@code newDefaultInstance} methods are an opt-out of + * JAXP pluggability: they pin the platform's built-in implementation instead of whatever a classpath lookup would resolve, which suits a library with + * minimal XML requirements that does not want to delegate the choice of implementation to the application developer. + *
+ ** An unresolved external reference resolves to empty content by default, so the parse continues without the resource. To reject it with an exception instead, * set the system property {@code org.apache.commons.xml.throwOnUnresolved} to {@code true}; the property is read at resolution time, and references resolved by * a caller-supplied resolver are unaffected. @@ -62,5 +75,4 @@ * be thread-safe. Create a new factory per thread or synchronize externally. *
*/ - package org.apache.commons.xml; diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 67b95b08..f8801f0a 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -66,7 +66,7 @@ so the parse continues without it ### Supported runtimes -The library requires OpenJDK 8 or later (or a JDK distribution built from it), or Android API level 19 or later. +The library requires OpenJDK 8 or later (or a JDK distribution built from it), or Android API level 26 or later. The security guarantees are defined only on the OpenJDK family (see the [Threat Model](threat_model.html)). @@ -146,6 +146,29 @@ HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) .validate(new StreamSource(inputStream)); ``` +### Factory methods + +Each factory class mirrors every static factory method its JAXP counterpart offers, +so a hardened factory is a drop-in replacement at any construction site: +the class-name/class-loader overloads and the StAX `newFactory` family (JDK 8), +`newDefaultInstance()` (Java 9, [JDK-8169778](https://bugs.openjdk.org/browse/JDK-8169778)), +and the namespace-aware `newNSInstance()` family (Java 13, [JDK-8223423](https://bugs.openjdk.org/browse/JDK-8223423)). + +All of these methods work on every supported runtime, including Java 8: +- The `newNSInstance` methods enable namespace awareness on their non-NS counterpart, + the behavior the JAXP methods are specified to have. +- The `newDefaultInstance` methods resolve the platform's own `newDefaultInstance` at run time + and use it wherever the runtime provides one — + Java 9 or later, and the Android API levels that ship the method — + falling back to instantiating the JDK's built-in implementation by class name on Java 8. + +The `newDefaultInstance` methods are an opt-out of JAXP pluggability: +they pin the platform's built-in implementation +instead of whatever a classpath lookup would resolve. +That suits a library with minimal XML requirements, +which can parse with the well-known platform parser +rather than delegate the choice of implementation to the application developer. + ### Stylesheets and schemas The hardening applies to documents parsed through the returned factory. Stylesheets given to diff --git a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java index eea5a927..0b4372f1 100644 --- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java +++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java @@ -21,21 +21,25 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.StringReader; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.transform.TransformerFactory; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPathFactory; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; /** * Public-API smoke tests for {@link org.apache.commons.xml}. @@ -131,4 +135,182 @@ void newXPathFactoryReturnsFreshInstance() throws Exception { assertNotSame(a, b); assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); } + + // The explicit-class-name tests discover the runtime default implementation through the raw JAXP factory, + // so they stay portable across the JAXP implementations of the surefire matrix. + @Test + void explicitClassNameDocumentBuilderFactoryIsHardened() throws Exception { + final Class> impl = DocumentBuilderFactory.newInstance().getClass(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameSAXParserFactoryIsHardened() throws Exception { + final Class> impl = SAXParserFactory.newInstance().getClass(); + final SAXParserFactory factory = HardeningSAXParserFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameSchemaFactoryIsHardened() throws Exception { + final Class> impl = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).getClass(); + final SchemaFactory factory = HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameTransformerFactoryIsHardened() { + final Class> impl = TransformerFactory.newInstance().getClass(); + final TransformerFactory factory = HardeningTransformerFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameXPathFactoryIsHardened() throws Exception { + final Class> impl = XPathFactory.newInstance().getClass(); + final XPathFactory factory = HardeningXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newFactoryReturnsFreshInstance() { + final XMLInputFactory a = HardeningXMLInputFactory.newFactory(); + final XMLInputFactory b = HardeningXMLInputFactory.newFactory(); + assertNotSame(a, b); + assertEquals(Boolean.TRUE, a.getProperty(XMLInputFactory.SUPPORT_DTD)); + } + + @Test + void factoryIdXMLInputFactoryIsHardened() { + final String factoryId = "org.apache.commons.xml.test.staxFactory"; + // XMLInputFactory.newInstance, not newFactory: Android's StAX API predates newFactory, and this file also compiles against android.jar. + System.setProperty(factoryId, XMLInputFactory.newInstance().getClass().getName()); + try { + final XMLInputFactory factory = HardeningXMLInputFactory.newFactory(factoryId, getClass().getClassLoader()); + assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD)); + } finally { + System.clearProperty(factoryId); + } + } + + @Test + void unknownFactoryClassNameThrows() { + assertThrows(FactoryConfigurationError.class, () -> HardeningDocumentBuilderFactory.newInstance("no.such.FactoryClass", null)); + } + + // The newDefault* methods resolve the Java 9 JAXP method at runtime and fall back to the JDK's built-in implementation on Java 8. The dom and sax + // variants also run on Android, whose JAXP predates newDefaultInstance and carries no JDK-internal fallback: the lookup miss surfaces there as the + // factory's own FactoryConfigurationError, like any newInstance miss. + @Test + @Tag("dom") + void newDefaultInstanceDocumentBuilderFactoryIsUsable() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningDocumentBuilderFactory::newDefaultInstance); + return; + } + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newDefaultInstance(); + assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + @Tag("sax") + void newDefaultInstanceSAXParserFactoryIsUsable() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningSAXParserFactory::newDefaultInstance); + return; + } + final SAXParserFactory factory = HardeningSAXParserFactory.newDefaultInstance(); + factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + // The newNSInstance family (Java 13) falls back to enabling namespace awareness on the corresponding newInstance lookup, the behavior the JAXP methods + // are specified to have, so the non-default variants work on every platform including Android. + @Test + @Tag("dom") + void newNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception { + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement()); + if (!AttackTestSupport.IS_ANDROID) { + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + } + + @Test + @Tag("dom") + void newDefaultNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningDocumentBuilderFactory::newDefaultNSInstance); + return; + } + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newDefaultNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + @Tag("sax") + void newNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception { + final SAXParserFactory factory = HardeningSAXParserFactory.newNSInstance(); + assertTrue(factory.isNamespaceAware()); + factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler()); + if (!AttackTestSupport.IS_ANDROID) { + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + } + + @Test + @Tag("sax") + void newDefaultNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningSAXParserFactory::newDefaultNSInstance); + return; + } + final SAXParserFactory factory = HardeningSAXParserFactory.newDefaultNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameNSDocumentBuilderFactoryIsNamespaceAware() throws Exception { + final Class> impl = DocumentBuilderFactory.newInstance().getClass(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameNSSAXParserFactoryIsNamespaceAware() throws Exception { + final Class> impl = SAXParserFactory.newInstance().getClass(); + final SAXParserFactory factory = HardeningSAXParserFactory.newNSInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultInstanceSchemaFactoryIsHardened() throws Exception { + final SchemaFactory factory = HardeningSchemaFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultInstanceTransformerFactoryIsHardened() { + final TransformerFactory factory = HardeningTransformerFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultFactoryXMLInputFactoryIsHardened() { + final XMLInputFactory factory = HardeningXMLInputFactory.newDefaultFactory(); + assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD)); + } + + @Test + void newDefaultInstanceXPathFactoryIsHardened() throws Exception { + final XPathFactory factory = HardeningXPathFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } }