From 63aeb8349c39fbe07f94ee32506f7f48300469c7 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 27 Aug 2026 21:40:36 +0200 Subject: [PATCH 1/6] 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 Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- .../xml/HardeningDocumentBuilderFactory.java | 29 ++++++-- .../xml/HardeningSAXParserFactory.java | 25 +++++-- .../commons/xml/HardeningSchemaFactory.java | 37 +++++++--- .../xml/HardeningTransformerFactory.java | 48 ++++++++----- .../commons/xml/HardeningXMLInputFactory.java | 32 ++++++++- .../commons/xml/HardeningXPathFactory.java | 70 ++++++++++++++----- .../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 1e88134a..e6d979f8 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. *

+ * 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. *

@@ -76,13 +83,6 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { /** * Returns a new, hardened {@link DocumentBuilderFactory}. - *

- * 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. - *

* * @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 27d91dbc..8dda4f20 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. *

+ * 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. *

@@ -152,12 +158,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 +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 a123b4a1..5a71d45e 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. *

+ * 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. *

@@ -61,17 +72,6 @@ static SchemaFactory harden(final SchemaFactory factory) { /** * Returns a new, hardened {@link SchemaFactory} for the given schema language. - *

- * 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}. - *

* * @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 172c9880..d80db4e4 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. *

+ * 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. *

@@ -95,23 +113,6 @@ 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. - *

- *

- * 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. - *

* * @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 37ed3932..907f94f4 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. *

+ * 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. *

@@ -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}. - *

- * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. - *

* * @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 eb33fafc..8b6eed85 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. *

+ * 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,25 +45,6 @@ */ 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()); - } - /** * 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 eea5a927..0847aa37 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)); + } } From 7e31532ea84023cdff7c68de98f8a3e21f409c5b Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 27 Aug 2026 22:40:12 +0200 Subject: [PATCH 2/6] Add the Java 9 newDefaultInstance factory methods, resolved at runtime Each factory class gains newDefaultInstance (newDefaultFactory for StAX) without raising the compile baseline: the Java 9 JAXP method is resolved through MethodHandles.publicLookup() and invoked when present; on Java 8 the JDK's built-in implementation is instantiated by class name instead. Where the platform provides neither, for example Android, the lookup miss surfaces as the factory's own configuration error, like any newInstance miss. java.lang.invoke raises the supported Android baseline to API level 26, and the bnd instructions drop the JDK-internal package inferred from the reflective StAX fallback. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- android-tests/build.gradle.kts | 3 +- pom.xml | 2 + .../xml/HardeningDocumentBuilderFactory.java | 46 ++++++++++++++++ .../xml/HardeningSAXParserFactory.java | 46 ++++++++++++++++ .../commons/xml/HardeningSchemaFactory.java | 49 +++++++++++++++++ .../xml/HardeningTransformerFactory.java | 48 +++++++++++++++++ .../commons/xml/HardeningXMLInputFactory.java | 53 ++++++++++++++++++- .../commons/xml/HardeningXPathFactory.java | 53 +++++++++++++++++++ .../org/apache/commons/xml/package-info.java | 2 +- src/site/markdown/index.md | 2 +- .../xml/HardeningFactoriesSmokeTest.java | 53 +++++++++++++++++++ 11 files changed, 353 insertions(+), 4 deletions(-) diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts index 9a47c92b..a5b75627 100644 --- a/android-tests/build.gradle.kts +++ b/android-tests/build.gradle.kts @@ -31,7 +31,8 @@ android { compileSdk = 34 defaultConfig { - minSdk = 19 + // java.lang.invoke, used by the newDefault* and newNS* lookups, exists from API level 26. + minSdk = 26 // androidx.test runner; Mannodermaus's android-junit5 plugin slots a JUnit 5 RunnerBuilder under it so AndroidJUnitRunner picks up Jupiter tests. testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/pom.xml b/pom.xml index 2999a192..4cdc6bba 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,9 @@ limitations under the License. org.apache.commons.xml org.apache.commons.xml.*;version=${project.version};-noimport:=true + + !com.sun.xml.internal.stream, net.sf.saxon.*;resolution:=optional, org.apache.xerces.*;resolution:=optional, * diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index e6d979f8..59091330 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.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; @@ -48,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. @@ -81,6 +97,36 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { return new Wrapper(factory); } + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the system-default implementation. + *

+ * 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 {@link DocumentBuilderFactory}. * diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index 8dda4f20..eb16defa 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; @@ -58,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. @@ -138,6 +154,36 @@ static XMLReader harden(final XMLReader reader) { return new HardeningXMLReader(reader); } + /** + * 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); + } + /** * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. * diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index 5a71d45e..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; @@ -55,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. * @@ -70,6 +89,36 @@ static SchemaFactory harden(final SchemaFactory factory) { return new Wrapper(factory); } + /** + * Returns a new, hardened {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0. + *

+ * 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. * diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index d80db4e4..a0ff62fa 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; @@ -74,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. * @@ -111,6 +129,36 @@ static TransformerFactory harden(final TransformerFactory factory) { return new Wrapper((SAXTransformerFactory) factory); } + /** + * Returns a new, hardened {@link TransformerFactory} of the system-default implementation. + *

+ * 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}. * diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java index 907f94f4..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; @@ -53,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. @@ -69,6 +86,40 @@ static XMLInputFactory harden(final XMLInputFactory factory) { return new Wrapper(factory); } + /** + * Returns a new, hardened {@link XMLInputFactory} of the system-default implementation. + *

+ * 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()}. * diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index 8b6eed85..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; @@ -45,6 +48,21 @@ */ public final class HardeningXPathFactory { + /** 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; + } + } + /** * Capability-driven hardening for any {@link XPathFactory} on the classpath. * @@ -83,6 +101,41 @@ 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. * 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..03764f40 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". *

*

diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 67b95b08..ad8057fb 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)). diff --git a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java index 0847aa37..69acdd88 100644 --- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java +++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java @@ -35,9 +35,11 @@ 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}. @@ -196,4 +198,55 @@ void factoryIdXMLInputFactoryIsHardened() { 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)); + } + + @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)); + } } From 506002149c6cca495fd013e6df123692cef11ec7 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Thu, 27 Aug 2026 22:40:12 +0200 Subject: [PATCH 3/6] Add the Java 13 newNSInstance factory methods, resolved at runtime DocumentBuilderFactory and SAXParserFactory gain the newNSInstance, newNSInstance(String, ClassLoader) and newDefaultNSInstance mirrors, resolved through MethodHandles.publicLookup() like the Java 9 methods. Where the platform predates them, the fallback enables namespace awareness on the corresponding newInstance lookup, the behavior the JAXP methods are specified to have, so the non-default variants work on every supported platform including Android; newDefaultNSInstance inherits the newDefaultInstance fallback chain. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- .../xml/HardeningDocumentBuilderFactory.java | 98 +++++++++++++++++++ .../xml/HardeningSAXParserFactory.java | 98 +++++++++++++++++++ .../xml/HardeningFactoriesSmokeTest.java | 64 ++++++++++++ 3 files changed, 260 insertions(+) diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index 59091330..aee284c5 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java @@ -56,6 +56,13 @@ public final class HardeningDocumentBuilderFactory { private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(DocumentBuilderFactory.class)); + private static final MethodHandle NEW_DEFAULT_NS_INSTANCE = findStatic("newDefaultNSInstance", MethodType.methodType(DocumentBuilderFactory.class)); + + private static final MethodHandle NEW_NS_INSTANCE = findStatic("newNSInstance", MethodType.methodType(DocumentBuilderFactory.class)); + + private static final MethodHandle NEW_NS_INSTANCE_BY_CLASS_NAME = findStatic("newNSInstance", + MethodType.methodType(DocumentBuilderFactory.class, String.class, ClassLoader.class)); + private static MethodHandle findStatic(final String name, final MethodType type) { try { return MethodHandles.publicLookup().findStatic(DocumentBuilderFactory.class, name, type); @@ -127,6 +134,36 @@ public static DocumentBuilderFactory newDefaultInstance() { return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null); } + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation. + *

+ * Obtained as by {@code DocumentBuilderFactory.newDefaultNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace + * awareness on {@link #newDefaultInstance()} otherwise, the behavior the JAXP method 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() { + if (NEW_DEFAULT_NS_INSTANCE != null) { + final DocumentBuilderFactory factory; + try { + factory = (DocumentBuilderFactory) NEW_DEFAULT_NS_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); + } + final DocumentBuilderFactory factory = newDefaultInstance(); + factory.setNamespaceAware(true); + return factory; + } + /** * Returns a new, hardened {@link DocumentBuilderFactory}. * @@ -156,6 +193,67 @@ public static DocumentBuilderFactory newInstance(final String factoryClassName, return harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); } + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory}. + *

+ * Obtained as by {@code DocumentBuilderFactory.newNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on + * {@link #newInstance()} otherwise, the behavior the JAXP method 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() { + if (NEW_NS_INSTANCE != null) { + final DocumentBuilderFactory factory; + try { + factory = (DocumentBuilderFactory) NEW_NS_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); + } + final DocumentBuilderFactory factory = newInstance(); + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class. + *

+ * Obtained as by {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} where the platform provides it (Java 13 or later), and by enabling + * namespace awareness on {@link #newInstance(String, ClassLoader)} otherwise, the behavior the JAXP method 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) { + if (NEW_NS_INSTANCE_BY_CLASS_NAME != null) { + final DocumentBuilderFactory factory; + try { + factory = (DocumentBuilderFactory) NEW_NS_INSTANCE_BY_CLASS_NAME.invokeExact(factoryClassName, classLoader); + } 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); + } + final DocumentBuilderFactory factory = newInstance(factoryClassName, classLoader); + factory.setNamespaceAware(true); + return factory; + } + /** * 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 eb16defa..36d45853 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -66,6 +66,13 @@ public final class HardeningSAXParserFactory { private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(SAXParserFactory.class)); + private static final MethodHandle NEW_DEFAULT_NS_INSTANCE = findStatic("newDefaultNSInstance", MethodType.methodType(SAXParserFactory.class)); + + private static final MethodHandle NEW_NS_INSTANCE = findStatic("newNSInstance", MethodType.methodType(SAXParserFactory.class)); + + private static final MethodHandle NEW_NS_INSTANCE_BY_CLASS_NAME = findStatic("newNSInstance", + MethodType.methodType(SAXParserFactory.class, String.class, ClassLoader.class)); + private static MethodHandle findStatic(final String name, final MethodType type) { try { return MethodHandles.publicLookup().findStatic(SAXParserFactory.class, name, type); @@ -184,6 +191,36 @@ public static SAXParserFactory newDefaultInstance() { return newInstance(JDK_SAX_PARSER_FACTORY, null); } + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the system-default implementation. + *

+ * Obtained as by {@code SAXParserFactory.newDefaultNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on + * {@link #newDefaultInstance()} otherwise, the behavior the JAXP method 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() { + if (NEW_DEFAULT_NS_INSTANCE != null) { + final SAXParserFactory factory; + try { + factory = (SAXParserFactory) NEW_DEFAULT_NS_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); + } + final SAXParserFactory factory = newDefaultInstance(); + factory.setNamespaceAware(true); + return factory; + } + /** * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. * @@ -227,6 +264,67 @@ public static SAXParserFactory newInstance(final String factoryClassName, final return harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); } + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory}. + *

+ * Obtained as by {@code SAXParserFactory.newNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on + * {@link #newInstance()} otherwise, the behavior the JAXP method 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() { + if (NEW_NS_INSTANCE != null) { + final SAXParserFactory factory; + try { + factory = (SAXParserFactory) NEW_NS_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); + } + final SAXParserFactory factory = newInstance(); + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class. + *

+ * Obtained as by {@code SAXParserFactory.newNSInstance(String, ClassLoader)} where the platform provides it (Java 13 or later), and by enabling namespace + * awareness on {@link #newInstance(String, ClassLoader)} otherwise, the behavior the JAXP method 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) { + if (NEW_NS_INSTANCE_BY_CLASS_NAME != null) { + final SAXParserFactory factory; + try { + factory = (SAXParserFactory) NEW_NS_INSTANCE_BY_CLASS_NAME.invokeExact(factoryClassName, classLoader); + } 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); + } + final SAXParserFactory factory = newInstance(factoryClassName, classLoader); + factory.setNamespaceAware(true); + return factory; + } + private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); diff --git a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java index 69acdd88..0b4372f1 100644 --- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java +++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java @@ -226,6 +226,70 @@ void newDefaultInstanceSAXParserFactoryIsUsable() throws Exception { 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(); From 3e27dba317162f3fde70a5b7deae604f5efd84c5 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 28 Aug 2026 08:41:11 +0200 Subject: [PATCH 4/6] Emulate the Java 13 NSInstance methods and reuse them internally The JDK implements the newNSInstance family by enabling namespace awareness on the result of the plain lookup; do the same with a private makeNSAware helper instead of resolving the platform methods through MethodHandles, and route the internal namespace-aware parser construction in newHardenedReader, hardenSourceToDom and HardeningXPath.parse through the public newNSInstance() methods. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ErRKq7RUeQ9LGrSboSUyYm --- .../xml/HardeningDocumentBuilderFactory.java | 88 +++++------------- .../xml/HardeningSAXParserFactory.java | 91 +++++-------------- .../xml/HardeningTransformerFactory.java | 3 +- .../apache/commons/xml/HardeningXPath.java | 3 +- 4 files changed, 44 insertions(+), 141 deletions(-) diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index aee284c5..1c16de63 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java @@ -56,13 +56,6 @@ public final class HardeningDocumentBuilderFactory { private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(DocumentBuilderFactory.class)); - private static final MethodHandle NEW_DEFAULT_NS_INSTANCE = findStatic("newDefaultNSInstance", MethodType.methodType(DocumentBuilderFactory.class)); - - private static final MethodHandle NEW_NS_INSTANCE = findStatic("newNSInstance", MethodType.methodType(DocumentBuilderFactory.class)); - - private static final MethodHandle NEW_NS_INSTANCE_BY_CLASS_NAME = findStatic("newNSInstance", - MethodType.methodType(DocumentBuilderFactory.class, String.class, ClassLoader.class)); - private static MethodHandle findStatic(final String name, final MethodType type) { try { return MethodHandles.publicLookup().findStatic(DocumentBuilderFactory.class, name, type); @@ -104,6 +97,17 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { return new Wrapper(factory); } + /** + * 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. *

@@ -135,11 +139,8 @@ public static DocumentBuilderFactory newDefaultInstance() { } /** - * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation. - *

- * Obtained as by {@code DocumentBuilderFactory.newDefaultNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace - * awareness on {@link #newDefaultInstance()} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -147,21 +148,7 @@ public static DocumentBuilderFactory newDefaultInstance() { * (for example Android). */ public static DocumentBuilderFactory newDefaultNSInstance() { - if (NEW_DEFAULT_NS_INSTANCE != null) { - final DocumentBuilderFactory factory; - try { - factory = (DocumentBuilderFactory) NEW_DEFAULT_NS_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); - } - final DocumentBuilderFactory factory = newDefaultInstance(); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newDefaultInstance()); } /** @@ -194,11 +181,8 @@ public static DocumentBuilderFactory newInstance(final String factoryClassName, } /** - * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory}. - *

- * Obtained as by {@code DocumentBuilderFactory.newNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on - * {@link #newInstance()} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -206,29 +190,13 @@ public static DocumentBuilderFactory newInstance(final String factoryClassName, * implementation is not available or cannot be instantiated. */ public static DocumentBuilderFactory newNSInstance() { - if (NEW_NS_INSTANCE != null) { - final DocumentBuilderFactory factory; - try { - factory = (DocumentBuilderFactory) NEW_NS_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); - } - final DocumentBuilderFactory factory = newInstance(); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newInstance()); } /** - * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class. - *

- * Obtained as by {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} where the platform provides it (Java 13 or later), and by enabling - * namespace awareness on {@link #newInstance(String, ClassLoader)} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -237,21 +205,7 @@ public static DocumentBuilderFactory newNSInstance() { * @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) { - if (NEW_NS_INSTANCE_BY_CLASS_NAME != null) { - final DocumentBuilderFactory factory; - try { - factory = (DocumentBuilderFactory) NEW_NS_INSTANCE_BY_CLASS_NAME.invokeExact(factoryClassName, classLoader); - } 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); - } - final DocumentBuilderFactory factory = newInstance(factoryClassName, classLoader); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newInstance(factoryClassName, classLoader)); } /** diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index 36d45853..9101acd1 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -66,13 +66,6 @@ public final class HardeningSAXParserFactory { private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(SAXParserFactory.class)); - private static final MethodHandle NEW_DEFAULT_NS_INSTANCE = findStatic("newDefaultNSInstance", MethodType.methodType(SAXParserFactory.class)); - - private static final MethodHandle NEW_NS_INSTANCE = findStatic("newNSInstance", MethodType.methodType(SAXParserFactory.class)); - - private static final MethodHandle NEW_NS_INSTANCE_BY_CLASS_NAME = findStatic("newNSInstance", - MethodType.methodType(SAXParserFactory.class, String.class, ClassLoader.class)); - private static MethodHandle findStatic(final String name, final MethodType type) { try { return MethodHandles.publicLookup().findStatic(SAXParserFactory.class, name, type); @@ -161,6 +154,17 @@ 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. *

@@ -192,11 +196,8 @@ public static SAXParserFactory newDefaultInstance() { } /** - * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the system-default implementation. - *

- * Obtained as by {@code SAXParserFactory.newDefaultNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on - * {@link #newDefaultInstance()} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -204,21 +205,7 @@ public static SAXParserFactory newDefaultInstance() { * (for example Android). */ public static SAXParserFactory newDefaultNSInstance() { - if (NEW_DEFAULT_NS_INSTANCE != null) { - final SAXParserFactory factory; - try { - factory = (SAXParserFactory) NEW_DEFAULT_NS_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); - } - final SAXParserFactory factory = newDefaultInstance(); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newDefaultInstance()); } /** @@ -231,9 +218,7 @@ public static SAXParserFactory newDefaultNSInstance() { */ 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); } @@ -265,11 +250,8 @@ public static SAXParserFactory newInstance(final String factoryClassName, final } /** - * Returns a new, hardened, namespace-aware {@link SAXParserFactory}. - *

- * Obtained as by {@code SAXParserFactory.newNSInstance()} where the platform provides it (Java 13 or later), and by enabling namespace awareness on - * {@link #newInstance()} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -277,29 +259,12 @@ public static SAXParserFactory newInstance(final String factoryClassName, final * error} or if the implementation is not available or cannot be instantiated. */ public static SAXParserFactory newNSInstance() { - if (NEW_NS_INSTANCE != null) { - final SAXParserFactory factory; - try { - factory = (SAXParserFactory) NEW_NS_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); - } - final SAXParserFactory factory = newInstance(); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newInstance()); } /** - * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class. - *

- * Obtained as by {@code SAXParserFactory.newNSInstance(String, ClassLoader)} where the platform provides it (Java 13 or later), and by enabling namespace - * awareness on {@link #newInstance(String, ClassLoader)} otherwise, the behavior the JAXP method is specified to have. - *

+ * 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. @@ -308,21 +273,7 @@ public static SAXParserFactory newNSInstance() { * @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) { - if (NEW_NS_INSTANCE_BY_CLASS_NAME != null) { - final SAXParserFactory factory; - try { - factory = (SAXParserFactory) NEW_NS_INSTANCE_BY_CLASS_NAME.invokeExact(factoryClassName, classLoader); - } 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); - } - final SAXParserFactory factory = newInstance(factoryClassName, classLoader); - factory.setNamespaceAware(true); - return factory; + return makeNSAware(newInstance(factoryClassName, classLoader)); } private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index a0ff62fa..7f030c56 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -244,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/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); From da5718d0edd93dca17285117023be9ddefab6a33 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 28 Aug 2026 09:00:34 +0200 Subject: [PATCH 5/6] Ignore the signature-polymorphic MethodHandle.invokeExact in animal-sniffer javac records the ad-hoc call-site descriptor for invokeExact, which no signature database lists, so the JDK 8 check flags a false positive on every MethodHandle lookup call site. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ErRKq7RUeQ9LGrSboSUyYm --- pom.xml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pom.xml b/pom.xml index 4cdc6bba..389972fe 100644 --- a/pom.xml +++ b/pom.xml @@ -130,6 +130,18 @@ limitations under the License. + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + + java.lang.invoke.MethodHandle + + + From a45add7397a3e3d76fe7c29445da7b69a8911111 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Fri, 28 Aug 2026 10:09:36 +0200 Subject: [PATCH 6/6] Document the JAXP factory methods and their Java 8 availability Describe on the site and in the package Javadoc the static factory methods mirrored from JAXP (the JDK 8 overloads, Java 9 newDefaultInstance and Java 13 newNSInstance families), all usable on Java 8, with newDefaultInstance as an opt-out of JAXP pluggability. Assisted-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01ErRKq7RUeQ9LGrSboSUyYm --- src/changes/changes.xml | 1 + .../org/apache/commons/xml/package-info.java | 14 ++++++++++- src/site/markdown/index.md | 23 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 09bbc39a..769d8635 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -40,6 +40,7 @@ The type attribute can be add, update, fix, or remove. Harden the SAXTransformerFactory extension surface (TransformerHandler, TemplatesHandler, and XMLFilter) and TransformerFactory.getAssociatedStylesheet. Document the threat model on the project site, including the denied-fetch contract and the supported runtime floor (OpenJDK 8 and Android API 33 or later). Add GitHub CI builds for Java 26 and 27-EA. + Mirror on each factory class every JAXP static factory method, including the Java 9 newDefaultInstance and Java 13 newNSInstance families, all usable on Java 8. Block XInclude (xi:include) href resolution by default, since the JAXP external-access properties do not govern it. Restore the hardened configuration when a factory or parser is reset() instead of reverting to the implementation defaults. 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 03764f40..c837c8c1 100644 --- a/src/main/java/org/apache/commons/xml/package-info.java +++ b/src/main/java/org/apache/commons/xml/package-info.java @@ -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: + *

+ *
    + *
  • the class-name/class-loader overloads and the StAX {@code newFactory} family (JDK 8),
  • + *
  • {@code newDefaultInstance()} (Java 9), and
  • + *
  • the namespace-aware {@code newNSInstance()} family (Java 13).
  • + *
+ *

+ * 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 ad8057fb..f8801f0a 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -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