This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch feature/jaxp-factory-methods in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit 776e2689fcc0ef72d512570eba0ad3e8287b8e8c Author: Piotr P. Karwasz <[email protected]> AuthorDate: Thu Aug 27 22:40:12 2026 +0200 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 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- android-tests/build.gradle.kts | 3 +- pom.xml | 2 + .../xml/HardeningDocumentBuilderFactory.java | 46 +++++++++++++++++++ .../commons/xml/HardeningSAXParserFactory.java | 46 +++++++++++++++++++ .../apache/commons/xml/HardeningSchemaFactory.java | 49 ++++++++++++++++++++ .../commons/xml/HardeningTransformerFactory.java | 48 ++++++++++++++++++++ .../commons/xml/HardeningXMLInputFactory.java | 53 +++++++++++++++++++++- .../apache/commons/xml/HardeningXPathFactory.java | 53 ++++++++++++++++++++++ .../java/org/apache/commons/xml/package-info.java | 2 +- src/site/markdown/index.md | 2 +- .../commons/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 9a47c92..a5b7562 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 2999a19..4cdc6bb 100644 --- a/pom.xml +++ b/pom.xml @@ -54,7 +54,9 @@ limitations under the License. <!-- OSGi bundle metadata: override commons-parent's org.apache.commons.* defaults. --> <commons.osgi.symbolicName>org.apache.commons.xml</commons.osgi.symbolicName> <commons.osgi.export>org.apache.commons.xml.*;version=${project.version};-noimport:=true</commons.osgi.export> + <!-- The negation drops the JDK-internal package bnd infers from the reflective Java 8 fallback in HardeningXMLInputFactory.newDefaultFactory(). --> <commons.osgi.import> + !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 e6d979f..5909133 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. + * <p> + * 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. + * </p> + * + * @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 8dda4f2..eb16def 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. + * <p> + * 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. + * </p> + * + * @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 5a71d45..539b8cf 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. + * <p> + * 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. + * </p> + * + * @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 d80db4e..a0ff62f 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. + * <p> + * 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. + * </p> + * + * @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 907f94f..6f15542 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. + * <p> + * 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. + * </p> + * + * @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 8b6eed8..cde63ab 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. + * <p> + * 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. + * </p> + * + * @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 c9e3f7a..03764f4 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 @@ * </ul> * <p> * 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". * </p> * <p> diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 67b95b0..ad8057f 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 0847aa3..69acdd8 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)); + } }
