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..389972fe 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, * @@ -128,6 +130,18 @@ limitations under the License. + + org.codehaus.mojo + animal-sniffer-maven-plugin + + + + java.lang.invoke.MethodHandle + + + 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/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index 1e88134a..1c16de63 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; @@ -31,6 +34,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. *

@@ -41,6 +51,19 @@ public final class HardeningDocumentBuilderFactory { /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */ private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_DOCUMENT_BUILDER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(DocumentBuilderFactory.class)); + + private static MethodHandle findStatic(final String name, final MethodType type) { + try { + return MethodHandles.publicLookup().findStatic(DocumentBuilderFactory.class, name, type); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link DocumentBuilderFactory} on the classpath. @@ -75,17 +98,64 @@ static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { } /** - * Returns a new, hardened {@link DocumentBuilderFactory}. + * Enables namespace awareness on the given factory; the {@code NSInstance} counterpart of each factory method routes its result through here. + * + * @param factory the factory to configure; never {@code null}. + * @return The given factory, namespace-aware. + */ + private static DocumentBuilderFactory makeNSAware(final DocumentBuilderFactory factory) { + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the system-default implementation. *

- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When - * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process - * {@code xi:include} elements but every external resource lookup is rejected. To permit specific trusted resources, install an - * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link DocumentBuilder} that allow-lists them; any href the resolver does not explicitly allow - * stays blocked. + * Obtained as by {@code DocumentBuilderFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and + * by instantiating the JDK's built-in implementation directly on Java 8. *

* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static DocumentBuilderFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final DocumentBuilderFactory factory; + try { + factory = (DocumentBuilderFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as the factory's own FactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_DOCUMENT_BUILDER_FACTORY, null); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation, enabling namespace awareness on + * {@link #newDefaultInstance()}, the behavior {@code DocumentBuilderFactory.newDefaultNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static DocumentBuilderFactory newDefaultNSInstance() { + return makeNSAware(newDefaultInstance()); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. * @throws IllegalStateException Thrown if a (non-Andoid) factory cannot support the secure processing feature * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the @@ -95,6 +165,49 @@ public static DocumentBuilderFactory newInstance() { return harden(DocumentBuilderFactory.newInstance()); } + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalStateException Thrown if a (non-Andoid) factory cannot support the secure processing feature + * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static DocumentBuilderFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory}, enabling namespace awareness on {@link #newInstance()}, the behavior + * {@code DocumentBuilderFactory.newNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the + * implementation is not available or cannot be instantiated. + */ + public static DocumentBuilderFactory newNSInstance() { + return makeNSAware(newInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class, enabling namespace awareness on + * {@link #newInstance(String, ClassLoader)}, the behavior {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified + * to have. + * + * @param factoryClassName The fully qualified class name of the {@link DocumentBuilderFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static DocumentBuilderFactory newNSInstance(final String factoryClassName, final ClassLoader classLoader) { + return makeNSAware(newInstance(factoryClassName, classLoader)); + } + /** * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. * diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index 27d91dbc..9101acd1 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -17,6 +17,9 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import javax.xml.XMLConstants; @@ -40,6 +43,12 @@ /** * Creates new, hardened {@link SAXParserFactory} instances. *

+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When + * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process {@code xi:include} + * elements but every external resource lookup is rejected. To permit specific trusted resources, install an {@link org.xml.sax.EntityResolver + * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists them; any href the resolver does not explicitly allow stays blocked. + *

+ *

* Not a {@link SAXParserFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *

@@ -52,6 +61,19 @@ public final class HardeningSAXParserFactory { private static final String ANDROID_EXPAT_READER = "org.apache.harmony.xml.ExpatReader"; /** Class name of Android's Harmony-based {@link SAXParserFactory}, backed by the native Expat parser. */ private static final String ANDROID_SAX_PARSER_FACTORY = "org.apache.harmony.xml.parsers.SAXParserFactoryImpl"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_SAX_PARSER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findStatic("newDefaultInstance", MethodType.methodType(SAXParserFactory.class)); + + private static MethodHandle findStatic(final String name, final MethodType type) { + try { + return MethodHandles.publicLookup().findStatic(SAXParserFactory.class, name, type); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link SAXParserFactory} on the classpath. @@ -132,6 +154,60 @@ static XMLReader harden(final XMLReader reader) { return new HardeningXMLReader(reader); } + /** + * Enables namespace awareness on the given factory; the {@code NSInstance} counterpart of each factory method routes its result through here. + * + * @param factory the factory to configure; never {@code null}. + * @return The given factory, namespace-aware. + */ + private static SAXParserFactory makeNSAware(final SAXParserFactory factory) { + factory.setNamespaceAware(true); + return factory; + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the system-default implementation. + *

+ * Obtained as by {@code SAXParserFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by + * instantiating the JDK's built-in implementation directly on Java 8. + *

+ * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SAXParserFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final SAXParserFactory factory; + try { + factory = (SAXParserFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as the factory's own FactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_SAX_PARSER_FACTORY, null); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the system-default implementation, enabling namespace awareness on + * {@link #newDefaultInstance()}, the behavior {@code SAXParserFactory.newDefaultNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SAXParserFactory newDefaultNSInstance() { + return makeNSAware(newDefaultInstance()); + } + /** * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. * @@ -142,9 +218,7 @@ static XMLReader harden(final XMLReader reader) { */ static XMLReader newHardenedReader() throws TransformerConfigurationException { try { - final SAXParserFactory factory = harden(SAXParserFactory.newInstance()); - factory.setNamespaceAware(true); - return factory.newSAXParser().getXMLReader(); + return newNSInstance().newSAXParser().getXMLReader(); } catch (final ParserConfigurationException | SAXException e) { throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e); } @@ -152,12 +226,6 @@ static XMLReader newHardenedReader() throws TransformerConfigurationException { /** * Returns a new, hardened {@link SAXParserFactory}. - *

- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, XInclude resolution is denied by default. When - * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process {@code xi:include} - * elements but every external resource lookup is rejected. To permit specific trusted resources, install an {@link org.xml.sax.EntityResolver - * EntityResolver} on the {@link org.xml.sax.XMLReader} that allow-lists them; any href the resolver does not explicitly allow stays blocked. - *

* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. @@ -168,6 +236,46 @@ public static SAXParserFactory newInstance() { return harden(SAXParserFactory.newInstance()); } + /** + * Returns a new, hardened {@link SAXParserFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link SAXParserFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static SAXParserFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory}, enabling namespace awareness on {@link #newInstance()}, the behavior + * {@code SAXParserFactory.newNSInstance()} (Java 13 or later) is specified to have. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown from {@link SAXParserFactory} in case of a {@link java.util.ServiceConfigurationError service configuration + * error} or if the implementation is not available or cannot be instantiated. + */ + public static SAXParserFactory newNSInstance() { + return makeNSAware(newInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class, enabling namespace awareness on + * {@link #newInstance(String, ClassLoader)}, the behavior {@code SAXParserFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified to have. + * + * @param factoryClassName The fully qualified class name of the {@link SAXParserFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static SAXParserFactory newNSInstance(final String factoryClassName, final ClassLoader classLoader) { + return makeNSAware(newInstance(factoryClassName, classLoader)); + } + private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index a123b4a1..539b8cfa 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -17,8 +17,12 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; +import javax.xml.XMLConstants; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.transform.Source; import javax.xml.transform.TransformerConfigurationException; @@ -36,6 +40,17 @@ /** * Creates new, hardened {@link SchemaFactory} instances. *

+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}: + *

+ *
    + *
  • {@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and
  • + *
  • {@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.
  • + *
+ *

+ * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the + * resulting {@link javax.xml.validation.Schema}. + *

+ *

* Not a {@link SchemaFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *

@@ -44,6 +59,21 @@ */ public final class HardeningSchemaFactory { + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_SCHEMA_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(SchemaFactory.class, "newDefaultInstance", + MethodType.methodType(SchemaFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } + /** * Hardening for any {@link SchemaFactory} on the classpath. * @@ -60,19 +90,38 @@ static SchemaFactory harden(final SchemaFactory factory) { } /** - * Returns a new, hardened {@link SchemaFactory} for the given schema language. + * Returns a new, hardened {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0. *

- * Beyond the three universal guarantees on {@link org.apache.commons.xml}: - *

- *
    - *
  • {@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and
  • - *
  • {@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.
  • - *
- *

- * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the - * resulting {@link javax.xml.validation.Schema}. + * Obtained as by {@code SchemaFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *

* + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalArgumentException Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation + * (for example Android). + */ + public static SchemaFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final SchemaFactory factory; + try { + factory = (SchemaFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final SchemaFactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as IllegalArgumentException, the error SchemaFactory.newInstance(String, String, ClassLoader) defines. + return newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, JDK_SCHEMA_FACTORY, null); + } + + /** + * Returns a new, hardened {@link SchemaFactory} for the given schema language. + * * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. * @return A hardened factory. * @throws IllegalArgumentException Thrown if no implementation of the schema language is available. @@ -83,6 +132,21 @@ public static SchemaFactory newInstance(final String schemaLanguage) { return harden(SchemaFactory.newInstance(schemaLanguage)); } + /** + * Returns a new, hardened {@link SchemaFactory} of the given implementation class. + * + * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. + * @param factoryClassName The fully qualified class name of the {@link SchemaFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalArgumentException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or instantiated, or does + * not support {@code schemaLanguage}. + * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. + */ + public static SchemaFactory newInstance(final String schemaLanguage, final String factoryClassName, final ClassLoader classLoader) { + return harden(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); + } + private HardeningSchemaFactory() { // static only } diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index 172c9880..7f030c56 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -18,6 +18,9 @@ package org.apache.commons.xml; import java.io.IOException; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import java.util.function.Supplier; @@ -31,6 +34,7 @@ import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.URIResolver; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.sax.SAXSource; @@ -48,6 +52,23 @@ /** * Creates new, hardened {@link TransformerFactory} instances. *

+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. + *

+ *

+ * The guarantees govern what the transform reads, not what it writes: an output instruction like {@code xsl:result-document} still writes wherever the + * stylesheet directs, so an untrusted stylesheet's output destinations must be restricted outside the library. + *

+ *

+ * The guarantees apply to every parser the factory creates internally for the standard {@link TransformerFactory} entry points: stylesheet compilation + * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) newTemplates(Source)}, + * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) newTransformer(Source)}) and source-document reading at + * {@code Transformer.transform(Source, Result)} time. + *

+ *

+ * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()}, + * {@code newXMLFilter(..)}), if reachable by casting the returned factory, produce objects carrying the same guarantees. + *

+ *

* Not a {@link TransformerFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *

@@ -56,6 +77,21 @@ */ public final class HardeningTransformerFactory { + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_TRANSFORMER_FACTORY = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(TransformerFactory.class, "newDefaultInstance", + MethodType.methodType(TransformerFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } + /** * Capability-driven hardening for any {@link TransformerFactory} on the classpath. * @@ -94,32 +130,58 @@ static TransformerFactory harden(final TransformerFactory factory) { } /** - * Returns a new, hardened {@link TransformerFactory}. - *

- * Beyond the three universal guarantees on {@link org.apache.commons.xml}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. - *

- *

- * The guarantees govern what the transform reads, not what it writes: an output instruction like {@code xsl:result-document} still writes wherever the - * stylesheet directs, so an untrusted stylesheet's output destinations must be restricted outside the library. - *

- *

- * The guarantees apply to every parser the factory creates internally for the standard {@link TransformerFactory} entry points: stylesheet compilation - * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) newTemplates(Source)}, - * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) newTransformer(Source)}) and source-document reading at - * {@code Transformer.transform(Source, Result)} time. - *

+ * Returns a new, hardened {@link TransformerFactory} of the system-default implementation. *

- * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()}, - * {@code newXMLFilter(..)}), if reachable by casting the returned factory, produce objects carrying the same guarantees. + * Obtained as by {@code TransformerFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *

* * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in + * implementation (for example Android). + */ + public static TransformerFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final TransformerFactory factory; + try { + factory = (TransformerFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final TransformerFactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. Where that class does not exist either (for + // example Android), the lookup miss surfaces as TransformerFactoryConfigurationError, like any newInstance miss. + return newInstance(JDK_TRANSFORMER_FACTORY, null); + } + + /** + * Returns a new, hardened {@link TransformerFactory}. + * + * @return A hardened factory. * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. */ public static TransformerFactory newInstance() { return harden(TransformerFactory.newInstance()); } + /** + * Returns a new, hardened {@link TransformerFactory} of the given implementation class. + * + * @param factoryClassName The fully qualified class name of the {@link TransformerFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if {@code factoryClassName} is {@code null} or the factory class cannot be loaded or instantiated. + */ + public static TransformerFactory newInstance(final String factoryClassName, final ClassLoader classLoader) { + return harden(TransformerFactory.newInstance(factoryClassName, classLoader)); + } + private static void setFeature(final TransformerFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); @@ -182,8 +244,7 @@ private static Source hardenSourceToDom(final Source source) throws TransformerC final InputSource inputSource = SAXSource.sourceToInputSource(source); if (inputSource != null) { try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); - factory.setNamespaceAware(true); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); final Document document = factory.newDocumentBuilder().parse(inputSource); return new DOMSource(document, inputSource.getSystemId()); } catch (final ParserConfigurationException | SAXException | IOException e) { diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java index 37ed3932..6f155423 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java @@ -19,10 +19,13 @@ import java.io.InputStream; import java.io.Reader; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; -import javax.xml.parsers.FactoryConfigurationError; import javax.xml.stream.EventFilter; +import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.StreamFilter; import javax.xml.stream.XMLEventReader; import javax.xml.stream.XMLInputFactory; @@ -36,6 +39,9 @@ /** * Creates new, hardened {@link XMLInputFactory} instances. *

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

+ *

* Not a {@link XMLInputFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultFactory()}. The hardened factories are instances of a nested, non-public wrapper class. *

@@ -50,6 +56,20 @@ public final class HardeningXMLInputFactory { static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver"; /** Woodstox property: resolver consulted for undeclared entity references. */ static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver"; + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultFactory()}. */ + private static final String JDK_XML_INPUT_FACTORY = "com.sun.xml.internal.stream.XMLInputFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_FACTORY = findNewDefaultFactory(); + + private static MethodHandle findNewDefaultFactory() { + try { + return MethodHandles.publicLookup().findStatic(XMLInputFactory.class, "newDefaultFactory", + MethodType.methodType(XMLInputFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } + } /** * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath. @@ -67,13 +87,70 @@ static XMLInputFactory harden(final XMLInputFactory factory) { } /** - * Returns a new, hardened {@link XMLInputFactory}. + * Returns a new, hardened {@link XMLInputFactory} of the system-default implementation. *

- * The three universal guarantees on {@link org.apache.commons.xml} apply; StAX exposes no additional vectors beyond them. + * Obtained as by {@code XMLInputFactory.newDefaultFactory()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. *

* * @return A hardened factory. * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if the running platform provides neither {@code newDefaultFactory()} nor the JDK's built-in implementation + * (for example Android). + */ + public static XMLInputFactory newDefaultFactory() { + if (NEW_DEFAULT_FACTORY != null) { + final XMLInputFactory factory; + try { + factory = (XMLInputFactory) NEW_DEFAULT_FACTORY.invokeExact(); + } catch (final FactoryConfigurationError e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + try { + // Java 8: the method does not exist, and XMLInputFactory has no class-name-taking lookup; instantiate the JDK's built-in default directly. + return harden((XMLInputFactory) Class.forName(JDK_XML_INPUT_FACTORY).getConstructor().newInstance()); + } catch (final ReflectiveOperationException e) { + // Where the class does not exist either (for example Android), report the miss like any StAX factory lookup: with FactoryConfigurationError. + throw new FactoryConfigurationError(e, "Neither XMLInputFactory.newDefaultFactory() nor " + JDK_XML_INPUT_FACTORY + " is available"); + } + } + + /** + * Returns a new, hardened {@link XMLInputFactory}, as by {@link XMLInputFactory#newFactory()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. + */ + public static XMLInputFactory newFactory() { + // XMLInputFactory.newInstance, not newFactory: the same specified lookup, but Android's StAX API predates newFactory. + return harden(XMLInputFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XMLInputFactory} resolved from the given factory id. + * + * @param factoryId The name of the factory to find; a system property or service id to look up, not the class name of the implementation. + * @param classLoader The class loader used in the lookup; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws FactoryConfigurationError Thrown in case of a service configuration error or if the implementation is not available or cannot be instantiated. + * @throws NullPointerException Thrown if {@code factoryId} is {@code null}. + */ + public static XMLInputFactory newFactory(final String factoryId, final ClassLoader classLoader) { + return harden(XMLInputFactory.newFactory(factoryId, classLoader)); + } + + /** + * Returns a new, hardened {@link XMLInputFactory}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. */ public static XMLInputFactory newInstance() { diff --git a/src/main/java/org/apache/commons/xml/HardeningXPath.java b/src/main/java/org/apache/commons/xml/HardeningXPath.java index abac0b73..c843fea6 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPath.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPath.java @@ -66,8 +66,7 @@ final class HardeningXPath implements XPath { static Document parse(final InputSource source) throws XPathExpressionException { Objects.requireNonNull(source, "source"); try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); - factory.setNamespaceAware(true); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); return factory.newDocumentBuilder().parse(source); } catch (final ParserConfigurationException | SAXException | IOException e) { throw new XPathExpressionException(e); diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index eb33fafc..cde63abb 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java @@ -17,6 +17,9 @@ package org.apache.commons.xml; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; import java.util.Objects; import javax.xml.XMLConstants; @@ -29,6 +32,14 @@ /** * Creates new, hardened {@link XPathFactory} instances. *

+ * Beyond the three universal guarantees on {@link org.apache.commons.xml}, URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()}, + * {@code unparsed-text()}) are not resolved. + *

+ *

+ * The guarantees also cover the document parse behind {@code XPath.evaluate(String, InputSource)} and {@code XPathExpression.evaluate(InputSource)}: the + * input document is built through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser. + *

+ *

* Not a {@link XPathFactory} itself, so none of the JAXP static factory methods is inherited: a caller cannot reach a non-hardened factory through this class * by calling an inherited method such as {@code newDefaultInstance()}. The hardened factories are instances of a nested, non-public wrapper class. *

@@ -37,23 +48,19 @@ */ public final class HardeningXPathFactory { - /** - * Returns a new, hardened {@link XPathFactory} for the default XPath object model. - *

- * Beyond the three universal guarantees on {@link org.apache.commons.xml}, URI-fetching XPath 3.1+ functions ({@code doc()}, {@code collection()}, - * {@code unparsed-text()}) are not resolved. - *

- *

- * The guarantees also cover the document parse behind {@code XPath.evaluate(String, InputSource)} and {@code XPathExpression.evaluate(InputSource)}: the - * input document is built through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} instead of the engine's internal parser. - *

- * - * @return A hardened factory. - * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. - * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. - */ - public static XPathFactory newInstance() { - return harden(XPathFactory.newInstance()); + /** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */ + private static final String JDK_XPATH_FACTORY = "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl"; + + private static final MethodHandle NEW_DEFAULT_INSTANCE = findNewDefaultInstance(); + + private static MethodHandle findNewDefaultInstance() { + try { + return MethodHandles.publicLookup().findStatic(XPathFactory.class, "newDefaultInstance", + MethodType.methodType(XPathFactory.class)); + } catch (final ReflectiveOperationException e) { + // The method is absent: the running platform predates it. + return null; + } } /** @@ -94,6 +101,84 @@ static XPathFactory harden(final XPathFactory factory) { return new Wrapper(factory); } + /** + * Returns a new, hardened {@link XPathFactory} of the system-default implementation, supporting the default XPath object model. + *

+ * Obtained as by {@code XPathFactory.newDefaultInstance()} where the platform provides it (Java 9 or later), and by instantiating the JDK's built-in + * implementation directly on Java 8. + *

+ * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws RuntimeException Thrown if the running platform provides neither {@code newDefaultInstance()} nor the JDK's built-in implementation (for + * example Android). + */ + public static XPathFactory newDefaultInstance() { + if (NEW_DEFAULT_INSTANCE != null) { + final XPathFactory factory; + try { + factory = (XPathFactory) NEW_DEFAULT_INSTANCE.invokeExact(); + } catch (final RuntimeException e) { + throw e; + } catch (final Throwable e) { + // Unreachable: the looked-up method declares no other exceptions. + throw new IllegalStateException(e); + } + return harden(factory); + } + try { + // Java 8: the method does not exist; instantiate the JDK's built-in default by its class name instead. + return newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, JDK_XPATH_FACTORY, null); + } catch (final XPathFactoryConfigurationException e) { + // newDefaultInstance declares no checked exception; mirror XPathFactory.newInstance(), which reports a default-model miss as a RuntimeException. + throw new RuntimeException( + "Neither XPathFactory.newDefaultInstance() nor " + JDK_XPATH_FACTORY + " is available", e); + } + } + + /** + * Returns a new, hardened {@link XPathFactory} for the default XPath object model. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. + */ + public static XPathFactory newInstance() { + return harden(XPathFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XPathFactory} for the given object model. + * + * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws XPathFactoryConfigurationException Thrown if no implementation of the object model is available. + * @throws NullPointerException Thrown if {@code uri} is {@code null}. + * @throws IllegalArgumentException Thrown if {@code uri} is empty. + */ + public static XPathFactory newInstance(final String uri) throws XPathFactoryConfigurationException { + return harden(XPathFactory.newInstance(uri)); + } + + /** + * Returns a new, hardened {@link XPathFactory} of the given implementation class. + * + * @param uri The underlying object model identifier, as accepted by {@link XPathFactory#newInstance(String)}. + * @param factoryClassName The fully qualified class name of the {@link XPathFactory} implementation. + * @param classLoader The class loader used to load the factory class; {@code null} means the current thread's context class loader. + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws XPathFactoryConfigurationException Thrown if {@code factoryClassName} is {@code null}, or if the factory class cannot be loaded or + * instantiated, or does not support {@code uri}. + * @throws NullPointerException Thrown if {@code uri} is {@code null}. + * @throws IllegalArgumentException Thrown if {@code uri} is empty. + */ + public static XPathFactory newInstance(final String uri, final String factoryClassName, final ClassLoader classLoader) + throws XPathFactoryConfigurationException { + return harden(XPathFactory.newInstance(uri, factoryClassName, classLoader)); + } + /** * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. * diff --git a/src/main/java/org/apache/commons/xml/package-info.java b/src/main/java/org/apache/commons/xml/package-info.java index c9e3f7a9..c837c8c1 100644 --- a/src/main/java/org/apache/commons/xml/package-info.java +++ b/src/main/java/org/apache/commons/xml/package-info.java @@ -33,7 +33,7 @@ * *

* These guarantees are defined on OpenJDK 8 or later (and JDK distributions built from it). No version of Android supports - * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android (API level 19 or later) the hardening is applied as best-effort without a guarantee, + * {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}, so on Android (API level 26 or later) the hardening is applied as best-effort without a guarantee, * tested as complete starting with API level 33; see the threat model's "Assumptions about the environment". *

*

@@ -46,6 +46,19 @@ * Each method adds factory-specific guarantees on top of the three above, documented on the corresponding {@code newXxxFactory()} method. *

*

+ * Each factory class mirrors every static factory method of its JAXP counterpart: + *

+ *
    + *
  • 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 67b95b08..f8801f0a 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -66,7 +66,7 @@ so the parse continues without it ### Supported runtimes -The library requires OpenJDK 8 or later (or a JDK distribution built from it), or Android API level 19 or later. +The library requires OpenJDK 8 or later (or a JDK distribution built from it), or Android API level 26 or later. The security guarantees are defined only on the OpenJDK family (see the [Threat Model](threat_model.html)). @@ -146,6 +146,29 @@ HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) .validate(new StreamSource(inputStream)); ``` +### Factory methods + +Each factory class mirrors every static factory method its JAXP counterpart offers, +so a hardened factory is a drop-in replacement at any construction site: +the class-name/class-loader overloads and the StAX `newFactory` family (JDK 8), +`newDefaultInstance()` (Java 9, [JDK-8169778](https://bugs.openjdk.org/browse/JDK-8169778)), +and the namespace-aware `newNSInstance()` family (Java 13, [JDK-8223423](https://bugs.openjdk.org/browse/JDK-8223423)). + +All of these methods work on every supported runtime, including Java 8: +- The `newNSInstance` methods enable namespace awareness on their non-NS counterpart, + the behavior the JAXP methods are specified to have. +- The `newDefaultInstance` methods resolve the platform's own `newDefaultInstance` at run time + and use it wherever the runtime provides one — + Java 9 or later, and the Android API levels that ship the method — + falling back to instantiating the JDK's built-in implementation by class name on Java 8. + +The `newDefaultInstance` methods are an opt-out of JAXP pluggability: +they pin the platform's built-in implementation +instead of whatever a classpath lookup would resolve. +That suits a library with minimal XML requirements, +which can parse with the well-known platform parser +rather than delegate the choice of implementation to the application developer. + ### Stylesheets and schemas The hardening applies to documents parsed through the returned factory. Stylesheets given to diff --git a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java index eea5a927..0b4372f1 100644 --- a/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java +++ b/src/test/java/org/apache/commons/xml/HardeningFactoriesSmokeTest.java @@ -21,21 +21,25 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.StringReader; import javax.xml.XMLConstants; import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.SAXParserFactory; import javax.xml.stream.XMLInputFactory; import javax.xml.transform.TransformerFactory; import javax.xml.validation.SchemaFactory; import javax.xml.xpath.XPathFactory; +import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import org.w3c.dom.Document; import org.xml.sax.InputSource; +import org.xml.sax.helpers.DefaultHandler; /** * Public-API smoke tests for {@link org.apache.commons.xml}. @@ -131,4 +135,182 @@ void newXPathFactoryReturnsFreshInstance() throws Exception { assertNotSame(a, b); assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); } + + // The explicit-class-name tests discover the runtime default implementation through the raw JAXP factory, + // so they stay portable across the JAXP implementations of the surefire matrix. + @Test + void explicitClassNameDocumentBuilderFactoryIsHardened() throws Exception { + final Class impl = DocumentBuilderFactory.newInstance().getClass(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameSAXParserFactoryIsHardened() throws Exception { + final Class impl = SAXParserFactory.newInstance().getClass(); + final SAXParserFactory factory = HardeningSAXParserFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameSchemaFactoryIsHardened() throws Exception { + final Class impl = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).getClass(); + final SchemaFactory factory = HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI, impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameTransformerFactoryIsHardened() { + final Class impl = TransformerFactory.newInstance().getClass(); + final TransformerFactory factory = HardeningTransformerFactory.newInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameXPathFactoryIsHardened() throws Exception { + final Class impl = XPathFactory.newInstance().getClass(); + final XPathFactory factory = HardeningXPathFactory.newInstance(XPathFactory.DEFAULT_OBJECT_MODEL_URI, impl.getName(), impl.getClassLoader()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newFactoryReturnsFreshInstance() { + final XMLInputFactory a = HardeningXMLInputFactory.newFactory(); + final XMLInputFactory b = HardeningXMLInputFactory.newFactory(); + assertNotSame(a, b); + assertEquals(Boolean.TRUE, a.getProperty(XMLInputFactory.SUPPORT_DTD)); + } + + @Test + void factoryIdXMLInputFactoryIsHardened() { + final String factoryId = "org.apache.commons.xml.test.staxFactory"; + // XMLInputFactory.newInstance, not newFactory: Android's StAX API predates newFactory, and this file also compiles against android.jar. + System.setProperty(factoryId, XMLInputFactory.newInstance().getClass().getName()); + try { + final XMLInputFactory factory = HardeningXMLInputFactory.newFactory(factoryId, getClass().getClassLoader()); + assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD)); + } finally { + System.clearProperty(factoryId); + } + } + + @Test + void unknownFactoryClassNameThrows() { + assertThrows(FactoryConfigurationError.class, () -> HardeningDocumentBuilderFactory.newInstance("no.such.FactoryClass", null)); + } + + // The newDefault* methods resolve the Java 9 JAXP method at runtime and fall back to the JDK's built-in implementation on Java 8. The dom and sax + // variants also run on Android, whose JAXP predates newDefaultInstance and carries no JDK-internal fallback: the lookup miss surfaces there as the + // factory's own FactoryConfigurationError, like any newInstance miss. + @Test + @Tag("dom") + void newDefaultInstanceDocumentBuilderFactoryIsUsable() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningDocumentBuilderFactory::newDefaultInstance); + return; + } + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newDefaultInstance(); + assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + @Tag("sax") + void newDefaultInstanceSAXParserFactoryIsUsable() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningSAXParserFactory::newDefaultInstance); + return; + } + final SAXParserFactory factory = HardeningSAXParserFactory.newDefaultInstance(); + factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + // The newNSInstance family (Java 13) falls back to enabling namespace awareness on the corresponding newInstance lookup, the behavior the JAXP methods + // are specified to have, so the non-default variants work on every platform including Android. + @Test + @Tag("dom") + void newNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception { + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertNotNull(factory.newDocumentBuilder().parse(new InputSource(new StringReader(BENIGN_XML))).getDocumentElement()); + if (!AttackTestSupport.IS_ANDROID) { + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + } + + @Test + @Tag("dom") + void newDefaultNSInstanceDocumentBuilderFactoryIsNamespaceAware() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningDocumentBuilderFactory::newDefaultNSInstance); + return; + } + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newDefaultNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + @Tag("sax") + void newNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception { + final SAXParserFactory factory = HardeningSAXParserFactory.newNSInstance(); + assertTrue(factory.isNamespaceAware()); + factory.newSAXParser().parse(new InputSource(new StringReader(BENIGN_XML)), new DefaultHandler()); + if (!AttackTestSupport.IS_ANDROID) { + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + } + + @Test + @Tag("sax") + void newDefaultNSInstanceSAXParserFactoryIsNamespaceAware() throws Exception { + if (AttackTestSupport.IS_ANDROID) { + assertThrows(FactoryConfigurationError.class, HardeningSAXParserFactory::newDefaultNSInstance); + return; + } + final SAXParserFactory factory = HardeningSAXParserFactory.newDefaultNSInstance(); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameNSDocumentBuilderFactoryIsNamespaceAware() throws Exception { + final Class impl = DocumentBuilderFactory.newInstance().getClass(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void explicitClassNameNSSAXParserFactoryIsNamespaceAware() throws Exception { + final Class impl = SAXParserFactory.newInstance().getClass(); + final SAXParserFactory factory = HardeningSAXParserFactory.newNSInstance(impl.getName(), impl.getClassLoader()); + assertTrue(factory.isNamespaceAware()); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultInstanceSchemaFactoryIsHardened() throws Exception { + final SchemaFactory factory = HardeningSchemaFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultInstanceTransformerFactoryIsHardened() { + final TransformerFactory factory = HardeningTransformerFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } + + @Test + void newDefaultFactoryXMLInputFactoryIsHardened() { + final XMLInputFactory factory = HardeningXMLInputFactory.newDefaultFactory(); + assertEquals(Boolean.TRUE, factory.getProperty(XMLInputFactory.SUPPORT_DTD)); + } + + @Test + void newDefaultInstanceXPathFactoryIsHardened() throws Exception { + final XPathFactory factory = HardeningXPathFactory.newDefaultInstance(); + assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)); + } }