This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch feature/jaxp-factory-methods in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit 31036ff2d63d6de69e7374e03d96c9a579c6ae04 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Thu Aug 27 21:40:36 2026 +0200 Add the JDK 8 JAXP static factory methods to the factory classes Mirror on each Hardening*Factory the static factory methods its JAXP counterpart offers in JDK 8, omitting only the deprecated XMLInputFactory.newInstance(String, ClassLoader): the explicit factoryClassName/ClassLoader overloads, the SchemaFactory and XPathFactory language/object-model variants, and the StAX newFactory family. Each is a one-liner through the class's hardening recipe. Assisted-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- .../xml/HardeningDocumentBuilderFactory.java | 29 ++++++--- .../commons/xml/HardeningSAXParserFactory.java | 25 ++++++-- .../apache/commons/xml/HardeningSchemaFactory.java | 37 ++++++++---- .../commons/xml/HardeningTransformerFactory.java | 48 +++++++++------ .../commons/xml/HardeningXMLInputFactory.java | 32 +++++++++- .../apache/commons/xml/HardeningXPathFactory.java | 70 ++++++++++++++++------ .../commons/xml/HardeningFactoriesSmokeTest.java | 65 ++++++++++++++++++++ 7 files changed, 243 insertions(+), 63 deletions(-) diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index 1e88134..e6d979f 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java @@ -31,6 +31,13 @@ /** * Creates new, hardened {@link DocumentBuilderFactory} instances. * <p> + * 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. + * </p> + * <p> * 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. * </p> @@ -76,13 +83,6 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { /** * Returns a new, hardened {@link DocumentBuilderFactory}. - * <p> - * 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. - * </p> * * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. @@ -95,6 +95,21 @@ 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)); + } + /** * 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 27d91db..8dda4f2 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -40,6 +40,12 @@ /** * Creates new, hardened {@link SAXParserFactory} instances. * <p> + * 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. + * </p> + * <p> * 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. * </p> @@ -152,12 +158,6 @@ static XMLReader newHardenedReader() throws TransformerConfigurationException { /** * Returns a new, hardened {@link SAXParserFactory}. - * <p> - * 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. - * </p> * * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. @@ -168,6 +168,19 @@ 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)); + } + 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 a123b4a..5a71d45 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -36,6 +36,17 @@ /** * Creates new, hardened {@link SchemaFactory} instances. * <p> + * Beyond the three universal guarantees on {@link org.apache.commons.xml}: + * </p> + * <ul> + * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and</li> + * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.</li> + * </ul> + * <p> + * 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}. + * </p> + * <p> * 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. * </p> @@ -61,17 +72,6 @@ static SchemaFactory harden(final SchemaFactory factory) { /** * Returns a new, hardened {@link SchemaFactory} for the given schema language. - * <p> - * Beyond the three universal guarantees on {@link org.apache.commons.xml}: - * </p> - * <ul> - * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and</li> - * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.</li> - * </ul> - * <p> - * 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}. - * </p> * * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. * @return A hardened factory. @@ -83,6 +83,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 172c988..d80db4e 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -31,6 +31,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 +49,23 @@ /** * Creates new, hardened {@link TransformerFactory} instances. * <p> + * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. + * </p> + * <p> + * 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. + * </p> + * <p> + * 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. + * </p> + * <p> + * 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. + * </p> + * <p> * 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. * </p> @@ -95,23 +113,6 @@ static TransformerFactory harden(final TransformerFactory factory) { /** * Returns a new, hardened {@link TransformerFactory}. - * <p> - * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. - * </p> - * <p> - * 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. - * </p> - * <p> - * 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. - * </p> - * <p> - * 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. - * </p> * * @return A hardened factory. * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. @@ -120,6 +121,19 @@ 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); diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java index 37ed393..907f94f 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java @@ -36,6 +36,9 @@ /** * Creates new, hardened {@link XMLInputFactory} instances. * <p> + * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. + * </p> + * <p> * 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. * </p> @@ -66,11 +69,34 @@ static XMLInputFactory harden(final XMLInputFactory factory) { return new Wrapper(factory); } + /** + * 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}. - * <p> - * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. - * </p> * * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index eb33faf..8b6eed8 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java @@ -29,6 +29,14 @@ /** * Creates new, hardened {@link XPathFactory} instances. * <p> + * 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. + * </p> + * <p> + * 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. + * </p> + * <p> * 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. * </p> @@ -37,25 +45,6 @@ */ public final class HardeningXPathFactory { - /** - * Returns a new, hardened {@link XPathFactory} for the default XPath object model. - * <p> - * 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. - * </p> - * <p> - * 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. - * </p> - * - * @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()); - } - /** * Capability-driven hardening for any {@link XPathFactory} on the classpath. * @@ -94,6 +83,49 @@ static XPathFactory harden(final XPathFactory factory) { return new Wrapper(factory); } + /** + * 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/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java index eea5a92..0847aa3 100644 --- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java +++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java @@ -21,12 +21,14 @@ 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; @@ -131,4 +133,67 @@ 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)); + } }
