diff --git a/pom.xml b/pom.xml
index db960273..82a2a6da 100644
--- a/pom.xml
+++ b/pom.xml
@@ -395,6 +395,73 @@ limitations under the License.
+ * How to enable: set {@code -Dorg.apache.commons.xml.throwOnUnresolved=true}. The property is read at resolution time, so it also applies to factories
+ * created before it was set; references resolved by a caller-supplied resolver are unaffected.
+ * Read per resolution, so the {@value XmlFactories#THROW_ON_UNRESOLVED} system property also toggles factories that already exist. Read per resolution, so the {@value #THROW_ON_UNRESOLVED} system property also toggles factories that already exist. Three layers cooperate: Used by providers whose underlying TrAX implementation pulls a new {@code SAXParserFactory.newInstance()} for any Source that is not already a
* {@link SAXSource} carrying its own {@link XMLReader}, and only sets {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING FSP} on the resulting reader.
- * Wrapping the factory and rewriting the Source upstream guarantees the parse runs through an {@link XmlFactories}-hardened reader instead.
Three layers cooperate:
*Each factory method mirrors the {@link DocumentBuilderFactory} static factory method of the same name and signature, and every returned factory carries + * the hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link DocumentBuilderFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeDocumentBuilderFactory { + + /** + * Returns a new, hardened {@link DocumentBuilderFactory}, obtained as by {@link DocumentBuilderFactory#newInstance()}. + * + * @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-Android) 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 + * implementation is not available or cannot be instantiated. + */ + public static DocumentBuilderFactory newInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the given implementation class, obtained as by + * {@link DocumentBuilderFactory#newInstance(String, ClassLoader)}. + * + * @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-Android) 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 DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); + } + + private SafeDocumentBuilderFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/SafeSAXParserFactory.java b/src/main/java/org/apache/commons/xml/SafeSAXParserFactory.java new file mode 100644 index 00000000..2c5f7529 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeSAXParserFactory.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.SAXParserFactory; + +/** + * Creates new, hardened {@link SAXParserFactory} instances. + * + *Each factory method mirrors the {@link SAXParserFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link SAXParserFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeSAXParserFactory { + + /** + * Returns a new, hardened {@link SAXParserFactory}, obtained as by {@link SAXParserFactory#newInstance()}. + * + * @return A hardened 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 newInstance() { + return SAXParserHardener.harden(SAXParserFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the given implementation class, obtained as by + * {@link SAXParserFactory#newInstance(String, ClassLoader)}. + * + * @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 SAXParserHardener.harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); + } + + private SafeSAXParserFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/SafeSchemaFactory.java b/src/main/java/org/apache/commons/xml/SafeSchemaFactory.java new file mode 100644 index 00000000..4007c6d8 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeSchemaFactory.java @@ -0,0 +1,76 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.SchemaFactoryConfigurationError; + +/** + * Creates new, hardened {@link SchemaFactory} instances. + * + *Each factory method mirrors the {@link SchemaFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees:
+ *+ * 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}. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link SchemaFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeSchemaFactory { + + /** + * Returns a new, hardened {@link SchemaFactory} for the given schema language, obtained as by {@link SchemaFactory#newInstance(String)}. + * + * @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. + * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. + * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. + */ + public static SchemaFactory newInstance(final String schemaLanguage) { + return SchemaHardener.harden(SchemaFactory.newInstance(schemaLanguage)); + } + + /** + * Returns a new, hardened {@link SchemaFactory} of the given implementation class, obtained as by + * {@link SchemaFactory#newInstance(String, String, ClassLoader)}. + * + * @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 SchemaHardener.harden(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); + } + + private SafeSchemaFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/SafeTransformerFactory.java b/src/main/java/org/apache/commons/xml/SafeTransformerFactory.java new file mode 100644 index 00000000..09e319ea --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeTransformerFactory.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; + +/** + * Creates new, hardened {@link TransformerFactory} instances. + * + *Each factory method mirrors the {@link TransformerFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees: {@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. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link TransformerFactory} method of the same name and + * returning a hardened factory.
+ */ +public final class SafeTransformerFactory { + + /** + * Returns a new, hardened {@link TransformerFactory}, obtained as by {@link TransformerFactory#newInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if the implementation is not available or cannot be instantiated. + */ + public static TransformerFactory newInstance() { + return TransformerHardener.harden(TransformerFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link TransformerFactory} of the given implementation class, obtained as by + * {@link TransformerFactory#newInstance(String, ClassLoader)}. + * + * @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 TransformerHardener.harden(TransformerFactory.newInstance(factoryClassName, classLoader)); + } + + private SafeTransformerFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/SafeXMLInputFactory.java b/src/main/java/org/apache/commons/xml/SafeXMLInputFactory.java new file mode 100644 index 00000000..3354764f --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeXMLInputFactory.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLInputFactory; + +/** + * Creates new, hardened {@link XMLInputFactory} instances. + * + *Each factory method mirrors the {@link XMLInputFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}. StAX exposes no additional vectors beyond the three universal + * guarantees.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultFactory()}, mirroring the {@link XMLInputFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeXMLInputFactory { + + /** + * Returns a new, hardened {@link XMLInputFactory}, obtained 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 StaxHardener.harden(XMLInputFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XMLInputFactory} resolved from the given factory id, obtained as by + * {@link XMLInputFactory#newFactory(String, ClassLoader)}. + *+ * The {@code factoryId} names a system property or service id to look up, same as {@link XMLInputFactory#newFactory(String, ClassLoader)}; it is not the + * class name of the implementation. + *
+ * + * @param factoryId The name of the factory to find; same treatment as a system property. + * @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 StaxHardener.harden(XMLInputFactory.newFactory(factoryId, classLoader)); + } + + private SafeXMLInputFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/SafeXPathFactory.java b/src/main/java/org/apache/commons/xml/SafeXPathFactory.java new file mode 100644 index 00000000..d41d53c7 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeXPathFactory.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathFactoryConfigurationException; + +/** + * Creates new, hardened {@link XPathFactory} instances. + * + *Each factory method mirrors the {@link XPathFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, 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. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link XPathFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeXPathFactory { + + /** + * Returns a new, hardened {@link XPathFactory} for the default XPath object model, obtained as by {@link XPathFactory#newInstance()}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XPathFactory} for the given object model, obtained as by {@link XPathFactory#newInstance(String)}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance(uri)); + } + + /** + * Returns a new, hardened {@link XPathFactory} of the given implementation class, obtained as by + * {@link XPathFactory#newInstance(String, String, ClassLoader)}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance(uri, factoryClassName, classLoader)); + } + + private SafeXPathFactory() { + // static only + } +} diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java b/src/main/java/org/apache/commons/xml/TransformerHardener.java index 5b937b58..0758bf34 100644 --- a/src/main/java/org/apache/commons/xml/TransformerHardener.java +++ b/src/main/java/org/apache/commons/xml/TransformerHardener.java @@ -40,8 +40,8 @@ * time and {@code document()} at runtime to an empty document, the one channel both XSLTC and Xalan route through. A caller-set {@link URIResolver} is * routed through the floor rather than replacing it, so a caller can opt a specific URI in but cannot reopen the fetch. *Every method on this class returns a new, hardened factory instance. No caching or pooling is performed; callers on a hot path are responsible - * for their own caching.
- * - *Every factory returned by this class makes the same three guarantees, regardless of which JAXP implementation is on the classpath:
- * - *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, - * tested as complete starting with API level 33; see the threat model's "Assumptions about the environment".
- * - *The guarantees hold whether or not the caller opts into DTD validation - * ({@link javax.xml.parsers.DocumentBuilderFactory#setValidating(boolean) setValidating(true)}) or attaches a compiled XSD via - * {@link javax.xml.parsers.DocumentBuilderFactory#setSchema(javax.xml.validation.Schema) setSchema}: every external resource the validation would otherwise - * fetch (the DTD itself, an {@code xsi:schemaLocation} hint, an external entity referenced from the DTD) remains blocked.
- * - *Each method on this class adds factory-specific guarantees on top of the three above, documented on the corresponding {@code newXxxFactory()} method.
- * - *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.
- * - *A top-level URI passed directly by the caller is fetched as-is: {@code StreamSource(systemId)}, {@code DocumentBuilder.parse(String)}, or a - * {@code SAXSource} built from a system id all cause the JAXP implementation to open that URI without consulting the hardening layer. Use a - * {@link javax.xml.transform.URIResolver} or {@link org.xml.sax.EntityResolver} if you need to restrict the top-level fetch.
- * - *The returned factories inherit the thread-safety properties of the underlying JAXP implementation, which in practice means they are not - * guaranteed to be thread-safe. Create a new factory per thread or synchronize externally.
- * - *This class itself is thread-safe: all methods are static and stateless.
- */ -public final class XmlFactories { - - /** - * System property that switches unresolved external references from the default empty resolution to a thrown exception. - *- * How to enable: set {@code -Dorg.apache.commons.xml.throwOnUnresolved=true}. The property is read at resolution time, so it also applies to factories - * created before it was set; references resolved by a caller-supplied resolver are unaffected. - *
- */ - static final String THROW_ON_UNRESOLVED = "org.apache.commons.xml.throwOnUnresolved"; - - /** - * Returns a new, hardened {@link DocumentBuilderFactory}. - *- * Beyond the three universal guarantees on {@link XmlFactories}, XInclude resolution is denied by default. When - * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on the returned factory, the parser will process - * {@code xi:include} elements but every external resource lookup is rejected. To permit specific trusted resources, install an - * {@link org.xml.sax.EntityResolver EntityResolver} on the {@link DocumentBuilder} that allow-lists them; any href the resolver does not explicitly allow - * stays blocked. - *
- * - * @return A hardened factory. - * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. - * @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 - * implementation is not available or cannot be instantiated. - */ - public static DocumentBuilderFactory newDocumentBuilderFactory() { - return DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); - } - - /** - * Returns a new, hardened {@link SAXParserFactory}. - *- * Beyond the three universal guarantees on {@link XmlFactories}, 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. - * @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 newSAXParserFactory() { - return SAXParserHardener.harden(SAXParserFactory.newInstance()); - } - - /** - * Returns a new, hardened {@link SchemaFactory} for the given schema language. - *- * Beyond the three universal guarantees on {@link XmlFactories}: - *
- *- * The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the - * resulting {@link javax.xml.validation.Schema}. - *
- * - * @param schemaLanguage The schema language, as accepted by {@link SchemaFactory#newInstance(String)}. - * @return A hardened factory. - * @throws IllegalArgumentException Thrown if no implementation of the schema language is available. - * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. - * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. - */ - public static SchemaFactory newSchemaFactory(final String schemaLanguage) { - return new HardeningSchemaFactory(SchemaFactory.newInstance(schemaLanguage)); - } - - /** - * Returns a new, hardened {@link TransformerFactory}. - *- * Beyond the three universal guarantees on {@link XmlFactories}: {@code xsl:import}, {@code xsl:include} and {@code document()} URIs are not resolved. - *
- *- * The guarantees govern what the transform reads, not what it writes: an output instruction like {@code xsl:result-document} still writes wherever the - * stylesheet directs, so an untrusted stylesheet's output destinations must be restricted outside the library. - *
- *- * The guarantees apply to every parser the factory creates internally for the standard {@link TransformerFactory} entry points: stylesheet compilation - * ({@link TransformerFactory#newTemplates(javax.xml.transform.Source) newTemplates(Source)}, - * {@link TransformerFactory#newTransformer(javax.xml.transform.Source) newTransformer(Source)}) and source-document reading at - * {@code Transformer.transform(Source, Result)} time. - *
- *- * The {@link javax.xml.transform.sax.SAXTransformerFactory} extension methods ({@code newTransformerHandler(..)}, {@code newTemplatesHandler()}, - * {@code newXMLFilter(..)}), if reachable by casting the returned factory, produce objects carrying the same guarantees. - *
- * - * @return A hardened factory. - * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. - */ - public static TransformerFactory newTransformerFactory() { - return TransformerHardener.harden(TransformerFactory.newInstance()); - } - - /** - * Returns a new, hardened {@link XMLInputFactory}. - *- * The three universal guarantees on {@link XmlFactories} apply; StAX exposes no additional vectors beyond them. - *
- * - * @return A hardened factory. - * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. - * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. - */ - public static XMLInputFactory newXMLInputFactory() { - return StaxHardener.harden(XMLInputFactory.newInstance()); - } - - /** - * Returns a new, hardened {@link XPathFactory} for the default XPath object model. - *- * Beyond the three universal guarantees on {@link XmlFactories}, 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 newXPathFactory() { - return XPathHardener.harden(XPathFactory.newInstance()); - } - - private XmlFactories() { - // static only - } -} 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 7cb62d34..484badf7 100644 --- a/src/main/java/org/apache/commons/xml/package-info.java +++ b/src/main/java/org/apache/commons/xml/package-info.java @@ -17,7 +17,58 @@ /** * Apache Commons XML provides secure-by-default JAXP factory creation for Java. A single method call returns a hardened JAXP factory that can be used to - * safely parse XML files. The entry point is the class {@link org.apache.commons.xml.XmlFactories}. + * safely parse XML files. The entry points are one factory class per JAXP factory type, whose methods mirror the JAXP static factory methods: + * + *Every method on these classes returns a new, hardened factory instance. No caching or pooling is performed; callers on a hot path are + * responsible for their own caching.
+ * + *Every factory returned by these classes makes the same three guarantees, regardless of which JAXP implementation is on the classpath:
+ * + *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, + * tested as complete starting with API level 33; see the threat model's "Assumptions about the environment".
+ * + *The guarantees hold whether or not the caller opts into DTD validation + * ({@link javax.xml.parsers.DocumentBuilderFactory#setValidating(boolean) setValidating(true)}) or attaches a compiled XSD via + * {@link javax.xml.parsers.DocumentBuilderFactory#setSchema(javax.xml.validation.Schema) setSchema}: every external resource the validation would otherwise + * fetch (the DTD itself, an {@code xsi:schemaLocation} hint, an external entity referenced from the DTD) remains blocked.
+ * + *Each factory class adds factory-specific guarantees on top of the three above, documented on the class itself.
+ * + *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.
+ * + *A top-level URI passed directly by the caller is fetched as-is: {@code StreamSource(systemId)}, {@code DocumentBuilder.parse(String)}, or a + * {@code SAXSource} built from a system id all cause the JAXP implementation to open that URI without consulting the hardening layer. Use a + * {@link javax.xml.transform.URIResolver} or {@link org.xml.sax.EntityResolver} if you need to restrict the top-level fetch.
+ * + *The returned factories inherit the thread-safety properties of the underlying JAXP implementation, which in practice means they are not + * guaranteed to be thread-safe. Create a new factory per thread or synchronize externally.
+ * + *The factory classes themselves are thread-safe: all methods are static and stateless.
*/ package org.apache.commons.xml; diff --git a/src/main/java13/org/apache/commons/xml/SafeDocumentBuilderFactory.java b/src/main/java13/org/apache/commons/xml/SafeDocumentBuilderFactory.java new file mode 100644 index 00000000..35e49c4e --- /dev/null +++ b/src/main/java13/org/apache/commons/xml/SafeDocumentBuilderFactory.java @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; + +/** + * Creates new, hardened {@link DocumentBuilderFactory} instances. + * + *Each factory method mirrors the {@link DocumentBuilderFactory} static factory method of the same name and signature, and every returned factory carries + * the hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link DocumentBuilderFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeDocumentBuilderFactory { + + /** + * Returns a new, hardened {@link DocumentBuilderFactory}, obtained as by {@link DocumentBuilderFactory#newInstance()}. + * + * @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-Android) 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 + * implementation is not available or cannot be instantiated. + */ + public static DocumentBuilderFactory newInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the given implementation class, obtained as by + * {@link DocumentBuilderFactory#newInstance(String, ClassLoader)}. + * + * @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-Android) 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 DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the system-default implementation, obtained as by + * {@link DocumentBuilderFactory#newDefaultInstance()}. + * + * @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-Android) factory cannot support the secure processing feature + * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + */ + public static DocumentBuilderFactory newDefaultInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newDefaultInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the system-default implementation, obtained as by + * {@link DocumentBuilderFactory#newDefaultNSInstance()}. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalStateException Thrown if a (non-Android) factory cannot support the secure processing feature + * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + */ + public static DocumentBuilderFactory newDefaultNSInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newDefaultNSInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory}, obtained as by {@link DocumentBuilderFactory#newNSInstance()}. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws IllegalStateException Thrown if a (non-Android) 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 + * implementation is not available or cannot be instantiated. + */ + public static DocumentBuilderFactory newNSInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newNSInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class, obtained as by + * {@link DocumentBuilderFactory#newNSInstance(String, ClassLoader)}. + * + * @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 IllegalStateException Thrown if a (non-Android) 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 newNSInstance(final String factoryClassName, final ClassLoader classLoader) { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newNSInstance(factoryClassName, classLoader)); + } + + private SafeDocumentBuilderFactory() { + // static only + } +} diff --git a/src/main/java13/org/apache/commons/xml/SafeSAXParserFactory.java b/src/main/java13/org/apache/commons/xml/SafeSAXParserFactory.java new file mode 100644 index 00000000..e30a14a2 --- /dev/null +++ b/src/main/java13/org/apache/commons/xml/SafeSAXParserFactory.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.SAXParserFactory; + +/** + * Creates new, hardened {@link SAXParserFactory} instances. + * + *Each factory method mirrors the {@link SAXParserFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link SAXParserFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeSAXParserFactory { + + /** + * Returns a new, hardened {@link SAXParserFactory}, obtained as by {@link SAXParserFactory#newInstance()}. + * + * @return A hardened 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 newInstance() { + return SAXParserHardener.harden(SAXParserFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the given implementation class, obtained as by + * {@link SAXParserFactory#newInstance(String, ClassLoader)}. + * + * @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 SAXParserHardener.harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the system-default implementation, obtained as by {@link SAXParserFactory#newDefaultInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static SAXParserFactory newDefaultInstance() { + return SAXParserHardener.harden(SAXParserFactory.newDefaultInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the system-default implementation, obtained as by + * {@link SAXParserFactory#newDefaultNSInstance()}. + * + * @return A hardened, namespace-aware factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static SAXParserFactory newDefaultNSInstance() { + return SAXParserHardener.harden(SAXParserFactory.newDefaultNSInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory}, obtained as by {@link SAXParserFactory#newNSInstance()}. + * + * @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 SAXParserHardener.harden(SAXParserFactory.newNSInstance()); + } + + /** + * Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class, obtained as by + * {@link SAXParserFactory#newNSInstance(String, ClassLoader)}. + * + * @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 SAXParserHardener.harden(SAXParserFactory.newNSInstance(factoryClassName, classLoader)); + } + + private SafeSAXParserFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeDocumentBuilderFactory.java b/src/main/java9/org/apache/commons/xml/SafeDocumentBuilderFactory.java new file mode 100644 index 00000000..7ec2aa7a --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeDocumentBuilderFactory.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.XMLConstants; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.FactoryConfigurationError; + +/** + * Creates new, hardened {@link DocumentBuilderFactory} instances. + * + *Each factory method mirrors the {@link DocumentBuilderFactory} static factory method of the same name and signature, and every returned factory carries + * the hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link DocumentBuilderFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link DocumentBuilderFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeDocumentBuilderFactory { + + /** + * Returns a new, hardened {@link DocumentBuilderFactory}, obtained as by {@link DocumentBuilderFactory#newInstance()}. + * + * @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-Android) 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 + * implementation is not available or cannot be instantiated. + */ + public static DocumentBuilderFactory newInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the given implementation class, obtained as by + * {@link DocumentBuilderFactory#newInstance(String, ClassLoader)}. + * + * @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-Android) 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 DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link DocumentBuilderFactory} of the system-default implementation, obtained as by + * {@link DocumentBuilderFactory#newDefaultInstance()}. + * + * @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-Android) factory cannot support the secure processing feature + * {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + */ + public static DocumentBuilderFactory newDefaultInstance() { + return DocumentBuilderHardener.harden(DocumentBuilderFactory.newDefaultInstance()); + } + + private SafeDocumentBuilderFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeSAXParserFactory.java b/src/main/java9/org/apache/commons/xml/SafeSAXParserFactory.java new file mode 100644 index 00000000..347f9271 --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeSAXParserFactory.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.parsers.FactoryConfigurationError; +import javax.xml.parsers.SAXParserFactory; + +/** + * Creates new, hardened {@link SAXParserFactory} instances. + * + *Each factory method mirrors the {@link SAXParserFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, XInclude resolution is denied by default. When + * {@link SAXParserFactory#setXIncludeAware(boolean) setXIncludeAware(true)} is called on a 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.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, and on Java 13 or later {@code newNSInstance()}, + * {@code newNSInstance(String, ClassLoader)} and {@code newDefaultNSInstance()}; each mirrors the {@link SAXParserFactory} method of the same name and + * returns a hardened factory.
+ */ +public final class SafeSAXParserFactory { + + /** + * Returns a new, hardened {@link SAXParserFactory}, obtained as by {@link SAXParserFactory#newInstance()}. + * + * @return A hardened 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 newInstance() { + return SAXParserHardener.harden(SAXParserFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the given implementation class, obtained as by + * {@link SAXParserFactory#newInstance(String, ClassLoader)}. + * + * @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 SAXParserHardener.harden(SAXParserFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link SAXParserFactory} of the system-default implementation, obtained as by {@link SAXParserFactory#newDefaultInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static SAXParserFactory newDefaultInstance() { + return SAXParserHardener.harden(SAXParserFactory.newDefaultInstance()); + } + + private SafeSAXParserFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeSchemaFactory.java b/src/main/java9/org/apache/commons/xml/SafeSchemaFactory.java new file mode 100644 index 00000000..13a73a94 --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeSchemaFactory.java @@ -0,0 +1,87 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.validation.SchemaFactory; +import javax.xml.validation.SchemaFactoryConfigurationError; + +/** + * Creates new, hardened {@link SchemaFactory} instances. + * + *Each factory method mirrors the {@link SchemaFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees:
+ *+ * 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}. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link SchemaFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeSchemaFactory { + + /** + * Returns a new, hardened {@link SchemaFactory} for the given schema language, obtained as by {@link SchemaFactory#newInstance(String)}. + * + * @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. + * @throws NullPointerException Thrown if {@code schemaLanguage} is {@code null}. + * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. + */ + public static SchemaFactory newInstance(final String schemaLanguage) { + return SchemaHardener.harden(SchemaFactory.newInstance(schemaLanguage)); + } + + /** + * Returns a new, hardened {@link SchemaFactory} of the given implementation class, obtained as by + * {@link SchemaFactory#newInstance(String, String, ClassLoader)}. + * + * @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 SchemaHardener.harden(SchemaFactory.newInstance(schemaLanguage, factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link SchemaFactory} of the system-default implementation, supporting W3C XML Schema 1.0, obtained as by + * {@link SchemaFactory#newDefaultInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static SchemaFactory newDefaultInstance() { + return SchemaHardener.harden(SchemaFactory.newDefaultInstance()); + } + + private SafeSchemaFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeTransformerFactory.java b/src/main/java9/org/apache/commons/xml/SafeTransformerFactory.java new file mode 100644 index 00000000..c5868bbc --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeTransformerFactory.java @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerFactoryConfigurationError; + +/** + * Creates new, hardened {@link TransformerFactory} instances. + * + *Each factory method mirrors the {@link TransformerFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees: {@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. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link TransformerFactory} method of the same name and + * returning a hardened factory.
+ */ +public final class SafeTransformerFactory { + + /** + * Returns a new, hardened {@link TransformerFactory}, obtained as by {@link TransformerFactory#newInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + * @throws TransformerFactoryConfigurationError Thrown if the implementation is not available or cannot be instantiated. + */ + public static TransformerFactory newInstance() { + return TransformerHardener.harden(TransformerFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link TransformerFactory} of the given implementation class, obtained as by + * {@link TransformerFactory#newInstance(String, ClassLoader)}. + * + * @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 TransformerHardener.harden(TransformerFactory.newInstance(factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link TransformerFactory} of the system-default implementation, obtained as by {@link TransformerFactory#newDefaultInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static TransformerFactory newDefaultInstance() { + return TransformerHardener.harden(TransformerFactory.newDefaultInstance()); + } + + private SafeTransformerFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeXMLInputFactory.java b/src/main/java9/org/apache/commons/xml/SafeXMLInputFactory.java new file mode 100644 index 00000000..fd64f2c1 --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeXMLInputFactory.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.stream.FactoryConfigurationError; +import javax.xml.stream.XMLInputFactory; + +/** + * Creates new, hardened {@link XMLInputFactory} instances. + * + *Each factory method mirrors the {@link XMLInputFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}. StAX exposes no additional vectors beyond the three universal + * guarantees.
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultFactory()}, mirroring the {@link XMLInputFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeXMLInputFactory { + + /** + * Returns a new, hardened {@link XMLInputFactory}, obtained 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 StaxHardener.harden(XMLInputFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XMLInputFactory} resolved from the given factory id, obtained as by + * {@link XMLInputFactory#newFactory(String, ClassLoader)}. + *+ * The {@code factoryId} names a system property or service id to look up, same as {@link XMLInputFactory#newFactory(String, ClassLoader)}; it is not the + * class name of the implementation. + *
+ * + * @param factoryId The name of the factory to find; same treatment as a system property. + * @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 StaxHardener.harden(XMLInputFactory.newFactory(factoryId, classLoader)); + } + + /** + * Returns a new, hardened {@link XMLInputFactory} of the system-default implementation, obtained as by {@link XMLInputFactory#newDefaultFactory()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static XMLInputFactory newDefaultFactory() { + return StaxHardener.harden(XMLInputFactory.newDefaultFactory()); + } + + private SafeXMLInputFactory() { + // static only + } +} diff --git a/src/main/java9/org/apache/commons/xml/SafeXPathFactory.java b/src/main/java9/org/apache/commons/xml/SafeXPathFactory.java new file mode 100644 index 00000000..ce9bbb4c --- /dev/null +++ b/src/main/java9/org/apache/commons/xml/SafeXPathFactory.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import javax.xml.xpath.XPathFactory; +import javax.xml.xpath.XPathFactoryConfigurationException; + +/** + * Creates new, hardened {@link XPathFactory} instances. + * + *Each factory method mirrors the {@link XPathFactory} static factory method of the same name and signature, and every returned factory carries the + * hardening guarantees documented for the {@link org.apache.commons.xml package}.
+ * + *Beyond the three universal guarantees, 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. + *
+ * + *On Java 9 or later the Multi-Release jar adds {@code newDefaultInstance()}, mirroring the {@link XPathFactory} method of the same name and returning a + * hardened factory.
+ */ +public final class SafeXPathFactory { + + /** + * Returns a new, hardened {@link XPathFactory} for the default XPath object model, obtained as by {@link XPathFactory#newInstance()}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance()); + } + + /** + * Returns a new, hardened {@link XPathFactory} for the given object model, obtained as by {@link XPathFactory#newInstance(String)}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance(uri)); + } + + /** + * Returns a new, hardened {@link XPathFactory} of the given implementation class, obtained as by + * {@link XPathFactory#newInstance(String, String, ClassLoader)}. + * + * @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 XPathHardener.harden(XPathFactory.newInstance(uri, factoryClassName, classLoader)); + } + + /** + * Returns a new, hardened {@link XPathFactory} of the system-default implementation, supporting the default XPath object model, obtained as by + * {@link XPathFactory#newDefaultInstance()}. + * + * @return A hardened factory. + * @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation. + */ + public static XPathFactory newDefaultInstance() { + return XPathHardener.harden(XPathFactory.newDefaultInstance()); + } + + private SafeXPathFactory() { + // static only + } +} diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index 0a88d1b3..9141b4a9 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -40,7 +40,7 @@ such as standalone Xerces, Woodstox, or Saxon's TrAX, need further configuration library author has no control over which implementation is on the classpath at runtime, so the effective security posture of their code depends on a deployment decision made elsewhere. -This library provides that baseline. Each `XmlFactories` call returns a new factory hardened by an +This library provides that baseline. Each call to one of its factory classes returns a new factory hardened by an implementation-specific recipe, so the returned object behaves the same way security-wise regardless of which JAXP implementation resolved. Security becomes a property of the call, not of the classpath, and there is one place to update when a new hardening setting becomes available or a default changes. @@ -57,14 +57,24 @@ Add the library to your build: ``` -Every method on `XmlFactories` returns a new, hardened factory. -Pick the one that matches the API you already use; +The library provides one factory class per JAXP factory type: +`SafeDocumentBuilderFactory`, `SafeSAXParserFactory`, `SafeSchemaFactory`, +`SafeTransformerFactory`, `SafeXMLInputFactory` and `SafeXPathFactory`. +Every method on them returns a new, hardened factory, +and the method names mirror the JAXP static factory methods. +Pick the class that matches the API you already use; no other configuration is required. On hardened factories an external resource reference (DTD, entity, schema, stylesheet) is never fetched: it resolves to empty content, so the parse continues without it (see Configuration below). +The jar is a Multi-Release jar, +so the factory classes grow with the platform: +on Java 9 or later they also expose `newDefaultInstance()` (`newDefaultFactory()` for StAX), +and on Java 13 or later `SafeDocumentBuilderFactory` and `SafeSAXParserFactory` add the `newNSInstance()` family, +each mirroring the JAXP factory method of the same name. + ### Supported runtimes The library requires OpenJDK 8 or later (or a JDK distribution built from it), or Android API level 19 or later. @@ -80,7 +90,7 @@ it is not a JAXP API. ### Supported implementations Out of the box the library recognizes the stock JDK JAXP implementations, Apache Xerces 2.x, Woodstox, and Saxon-HE. If -a factory resolves to an implementation not covered by any bundled hardening recipe, every `XmlFactories` method throws +a factory resolves to an implementation not covered by any bundled hardening recipe, every factory method throws `IllegalStateException` with a message naming the unsupported class. Adding support for a new JAXP implementation requires a code change to this library. @@ -88,26 +98,26 @@ requires a code change to this library. ```java import org.w3c.dom.Document; -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeDocumentBuilderFactory; -Document doc = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputStream); +Document doc = SafeDocumentBuilderFactory.newInstance().newDocumentBuilder().parse(inputStream); ``` **SAX parsing** via `SAXParserFactory`: ```java -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeSAXParserFactory; -XmlFactories.newSAXParserFactory().newSAXParser().parse(inputStream, myDefaultHandler); +SafeSAXParserFactory.newInstance().newSAXParser().parse(inputStream, myDefaultHandler); ``` **Streaming (StAX) parsing** via `XMLInputFactory`: ```java import javax.xml.stream.XMLStreamReader; -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeXMLInputFactory; -XMLStreamReader reader = XmlFactories.newXMLInputFactory().createXMLStreamReader(inputStream); +XMLStreamReader reader = SafeXMLInputFactory.newFactory().createXMLStreamReader(inputStream); ``` **XSLT transforms** via `TransformerFactory`: @@ -115,9 +125,9 @@ XMLStreamReader reader = XmlFactories.newXMLInputFactory().createXMLStreamReader ```java import javax.xml.transform.stream.StreamSource; import javax.xml.transform.stream.StreamResult; -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeTransformerFactory; -XmlFactories.newTransformerFactory() +SafeTransformerFactory.newInstance() .newTransformer(new StreamSource(stylesheet)) .transform(new StreamSource(inputStream), new StreamResult(outputStream)); ``` @@ -127,9 +137,9 @@ XmlFactories.newTransformerFactory() ```java import javax.xml.xpath.XPathConstants; import org.w3c.dom.NodeList; -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeXPathFactory; -NodeList hits = (NodeList) XmlFactories.newXPathFactory() +NodeList hits = (NodeList) SafeXPathFactory.newInstance() .newXPath() .evaluate("//item", doc, XPathConstants.NODESET); ``` @@ -139,9 +149,9 @@ NodeList hits = (NodeList) XmlFactories.newXPathFactory() ```java import javax.xml.XMLConstants; import javax.xml.transform.stream.StreamSource; -import org.apache.commons.xml.XmlFactories; +import org.apache.commons.xml.SafeSchemaFactory; -XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI) +SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) .newSchema(new StreamSource(xsdStream)) .newValidator() .validate(new StreamSource(inputStream)); @@ -152,7 +162,8 @@ XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI) The hardening applies to documents parsed through the returned factory. Stylesheets given to `TransformerFactory.newTransformer(Source)` and schemas given to `SchemaFactory.newSchema(Source)` are read by a parser the implementation picks internally, and that parser may not be hardened (Saxon's TrAX is one such case, see Building -below). Treat stylesheets and schemas as trusted input, or pre-parse them through a hardened `XmlFactories` parser and +below). Treat stylesheets and schemas as trusted input, +or pre-parse them through a hardened `SafeDocumentBuilderFactory` or `SafeSAXParserFactory` parser and pass the result as a `DOMSource` or `SAXSource`. A stylesheet also chooses where the transform writes (`xsl:result-document`): the hardening governs reads only, @@ -162,7 +173,7 @@ so restrict output destinations yourself when running an untrusted stylesheet ### Transformer handlers and filters The `SAXTransformerFactory` extension methods, `newTransformerHandler(...)`, `newTemplatesHandler()` and `newXMLFilter(...)`, -if reachable by casting the factory from `XmlFactories.newTransformerFactory()`, +if reachable by casting the factory from `SafeTransformerFactory.newInstance()`, produce handlers, filters and `Templates` carrying the same hardening as the standard entry points: runtime `document()` resolves to empty content, and a filter with no caller-set parent parses its input through a hardened reader. @@ -172,7 +183,7 @@ See the [Threat Model](threat_model.html) for the exact scope. ### Caching and thread-safety -There is no caching or pooling inside `XmlFactories`; callers on a hot path are responsible for their own caching. The +There is no caching or pooling inside the factory classes; callers on a hot path are responsible for their own caching. The returned factories inherit the thread-safety properties of the underlying JAXP implementation, which in practice means they are not thread-safe. Create a new factory per thread or synchronize externally. diff --git a/src/site/markdown/threat_model.md b/src/site/markdown/threat_model.md index 1a688c17..0c9cf144 100644 --- a/src/site/markdown/threat_model.md +++ b/src/site/markdown/threat_model.md @@ -40,12 +40,12 @@ a finding that falls under [What is out of scope](#what-is-out-of-scope) will be ### Scope and intended use -This library is a helper for **safely creating JAXP factories**. Each `XmlFactories.newXxxFactory()` method returns a +This library is a helper for **safely creating JAXP factories**. Each method of its `Safe*` factory classes returns a new, hardened factory whose parsers reject the common XML attacks (external entity / DTD resolution, XXE, SSRF through external references, and entity-expansion denial of service such as Billion Laughs). The exact guarantee each factory makes is documented in the Javadoc: -https://commons.apache.org/sandbox/commons-xml/apidocs/org/apache/commons/xml/factory/XmlFactories.html +https://commons.apache.org/sandbox/commons-xml/apidocs/org/apache/commons/xml/package-summary.html The hardening applies to the factory and to the parsers, readers, transformers, validators, schemas and XPath objects it produces. It governs what those objects read; @@ -59,7 +59,7 @@ document tries to reach through an entity, DTD, schema, stylesheet, or XInclude exists to stop that untrusted document from reading local resources, reaching the network, or exhausting memory or CPU. -The trust boundary is the factory as returned by `XmlFactories`. The XML handed to a parser, reader, +The trust boundary is the factory as returned by the library's `Safe*` factory classes. The XML handed to a parser, reader, transformer, validator or schema produced by that factory is **untrusted**; the configuration of the factory is **trusted**, and keeping it as delivered is the caller's responsibility. A caller running in the same process can always reconfigure or replace the factory, so such a caller is not an adversary this model @@ -77,7 +77,7 @@ because your reader's settings are indistinguishable from configuration you chos ### What is in scope -- The hardening recipes applied by `XmlFactories`. +- The hardening recipes applied by the factory classes. Every implementation of JAXP 1.4 or later is in scope, as long as it respects the contract of the features, attributes, and properties the recipes use. An implementation that cannot accept a required setting makes the factory method throw @@ -85,7 +85,7 @@ because your reader's settings are indistinguishable from configuration you chos The recipes for Android's Expat/KXmlParser are applied as best-effort and carry no guarantee (see **Supported runtimes** under [Assumptions about the environment](#assumptions-about-the-environment)). -- A factory returned by `XmlFactories`, used as delivered, that fails to provide a guarantee the Javadoc states it +- A factory returned by the library, used as delivered, that fails to provide a guarantee the Javadoc states it provides. The guarantee covers the documented entry points of each returned factory type, including the `SAXTransformerFactory` extension methods when the returned `TransformerFactory` exposes them. @@ -95,7 +95,7 @@ The library does not open network connections, spawn processes, install signal handlers, or read environment variables of its own: -each `XmlFactories` method only configures and returns a JAXP factory. +each factory method only configures and returns a JAXP factory. Which hardening recipe applies depends on the JAXP implementation present on the classpath. **Supported runtimes** @@ -234,13 +234,13 @@ and reports against a factory reconfigured in any of the ways below are out of s `StreamSource(systemId)`, a `SAXSource` built from a system id) is fetched as-is by the JAXP implementation without consulting the hardening layer. Restrict it yourself if the URI is untrusted. - **Caller-supplied parser instances.** - A parser built outside `XmlFactories` and handed to a produced instance is used as configured: + A parser built outside the library and handed to a produced instance is used as configured: a `SAXSource` carrying its own `XMLReader`, a `StAXSource` carrying a stream or event reader, or a `DOMSource` holding a document parsed elsewhere. Its settings are yours, including permissive ones. To parse with your own reader under the hardening guarantees, - obtain it from `XmlFactories.newSAXParserFactory()` + obtain it from `SafeSAXParserFactory.newInstance()` before wrapping it in a `SAXSource`. - The behavior of a JAXP implementation that does not respect the contract of the settings a hardening recipe requires (the factory method throws rather than returning an unhardened factory), @@ -270,7 +270,7 @@ re-establishing any protection you remove. XML-security scanners and static analyzers routinely flag the parsers this library produces. The following are **not** vulnerabilities under this model: -- A claim that a factory or instance produced by `XmlFactories` is unsafe, without showing that a reserved +- A claim that a factory or instance produced by the library is unsafe, without showing that a reserved setting was loosened, a resolver was installed, or an untrusted top-level URI was passed (see [Assumptions about the environment](#assumptions-about-the-environment) and [What is out of scope](#what-is-out-of-scope)). As delivered, the instance is hardened; the bare presence @@ -296,7 +296,7 @@ are **not** vulnerabilities under this model: instruction of a stylesheet (see **Transform output destinations** under [What is out of scope](#what-is-out-of-scope)). - Reports in a JAXP implementation that does not respect the contract of the settings a hardening recipe - requires: `XmlFactories` throws rather than returning an unhardened factory, so there is no instance to attack. + requires: the factory method throws rather than returning an unhardened factory, so there is no instance to attack. ### Triage dispositions @@ -314,7 +314,7 @@ A report judged against this model receives exactly one of: ### Conditions that would change this model Revise this model when any of the following change: -a new `XmlFactories` factory method or other public surface; +a new factory method or other public surface; support for a JAXP implementation beyond those listed under [What is in scope](#what-is-in-scope); a change to the supported runtimes (see **Supported runtimes** under [Assumptions about the environment](#assumptions-about-the-environment)); a new reserved setting; diff --git a/src/test/java/org/apache/commons/xml/AssociatedStylesheetTest.java b/src/test/java/org/apache/commons/xml/AssociatedStylesheetTest.java index 3a553fd0..b55d39a4 100644 --- a/src/test/java/org/apache/commons/xml/AssociatedStylesheetTest.java +++ b/src/test/java/org/apache/commons/xml/AssociatedStylesheetTest.java @@ -50,7 +50,7 @@ private static void assertAssociatedStylesheet(final Source associated) { } private static TransformerFactory hardenedFactory() { - final TransformerFactory factory = XmlFactories.newTransformerFactory(); + final TransformerFactory factory = SafeTransformerFactory.newInstance(); factory.setErrorListener(AttackTestSupport.STRICT_REPORTER); return factory; } diff --git a/src/test/java/org/apache/commons/xml/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/AttackTestSupport.java index a5d87b25..20090e8f 100644 --- a/src/test/java/org/apache/commons/xml/AttackTestSupport.java +++ b/src/test/java/org/apache/commons/xml/AttackTestSupport.java @@ -61,7 +61,7 @@ *The hardened-side helpers come in three flavors, distinguished by their suffix:
* *{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; only a thrown exception passes.
+ *{@link DocumentBuilder#parse(InputSource)} via {@link SafeDocumentBuilderFactory#newInstance()}; only a thrown exception passes.
*/ static void assertDomBlocks(final String payload) { - assertParseFails(() -> strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)), "DOM", SAXException.class); + assertParseFails(() -> strictDocumentBuilder(SafeDocumentBuilderFactory.newInstance()).parse(inputSource(payload)), "DOM", SAXException.class); } /** @@ -226,7 +226,7 @@ static void assertDomBlocksOrDoesNotLeak(final String payload) { /** * Asserts a hardened DOM parse completes without throwing and without leaked content. * - *{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; use this when the hardening guarantee is "the parse + *
{@link DocumentBuilder#parse(InputSource)} via {@link SafeDocumentBuilderFactory#newInstance()}; use this when the hardening guarantee is "the parse * succeeds but never resolves the external resource", for example, when the ignore-all resolver floor resolves the external subset to empty content.
*/ static void assertDomDoesNotLeak(final String payload) { @@ -236,10 +236,10 @@ static void assertDomDoesNotLeak(final String payload) { /** * Asserts a hardened DOM parse succeeds. * - *{@link DocumentBuilder#parse(InputSource)} via {@link XmlFactories#newDocumentBuilderFactory()}; positive control for DOCTYPE-only payloads.
+ *{@link DocumentBuilder#parse(InputSource)} via {@link SafeDocumentBuilderFactory#newInstance()}; positive control for DOCTYPE-only payloads.
*/ static void assertDomParses(final String payload) { - assertParseSucceeds(() -> strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)), "DOM"); + assertParseSucceeds(() -> strictDocumentBuilder(SafeDocumentBuilderFactory.newInstance()).parse(inputSource(payload)), "DOM"); } /** @@ -444,45 +444,45 @@ static void assertPermissiveValidatorValidates(final String xml) { /** * Asserts a hardened SAX parse of the payload throws. * - *{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; only a thrown exception passes.
+ *{@link XMLReader#parse(InputSource)} on a parser from {@link SafeSAXParserFactory#newInstance()}; only a thrown exception passes.
*/ static void assertSaxBlocks(final String payload) { - assertParseFails(() -> consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX", SAXException.class); + assertParseFails(() -> consumeXmlReader(strictXMLReader(SafeSAXParserFactory.newInstance()), payload), "SAX", SAXException.class); } /** * Asserts a hardened SAX parse either blocks at parse or completes without leaked content. See {@link #assertDomBlocksOrDoesNotLeak(String)}. */ static void assertSaxBlocksOrDoesNotLeak(final String payload) { - assertNoLeakOrThrows(() -> captureCharacters(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX", SAXException.class); + assertNoLeakOrThrows(() -> captureCharacters(strictXMLReader(SafeSAXParserFactory.newInstance()), payload), "SAX", SAXException.class); } /** * Asserts a hardened SAX parse completes without throwing and without leaked content. * - *{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; use this when the hardening guarantee is "the parse + *
{@link XMLReader#parse(InputSource)} on a parser from {@link SafeSAXParserFactory#newInstance()}; use this when the hardening guarantee is "the parse * succeeds but never resolves the external resource", for example, when the ignore-all resolver floor resolves the external subset to empty content.
*/ static void assertSaxDoesNotLeak(final String payload) { - assertNoLeakStrict(() -> captureCharacters(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX"); + assertNoLeakStrict(() -> captureCharacters(strictXMLReader(SafeSAXParserFactory.newInstance()), payload), "SAX"); } /** * Asserts a hardened SAX parse succeeds. * - *{@link XMLReader#parse(InputSource)} on a parser from {@link XmlFactories#newSAXParserFactory()}; positive control for DOCTYPE-only payloads.
+ *{@link XMLReader#parse(InputSource)} on a parser from {@link SafeSAXParserFactory#newInstance()}; positive control for DOCTYPE-only payloads.
*/ static void assertSaxParses(final String payload) { - assertParseSucceeds(() -> consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), "SAX"); + assertParseSucceeds(() -> consumeXmlReader(strictXMLReader(SafeSAXParserFactory.newInstance()), payload), "SAX"); } /** * Asserts a hardened Schema compilation throws. * - *{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; only a thrown exception passes.
+ *{@link SchemaFactory#newSchema(Source)} via {@link SafeSchemaFactory#newInstance(String)}; only a thrown exception passes.
*/ static void assertSchemaBlocks(final Source xsd) { - assertParseFails(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile", SAXException.class, SecurityException.class); + assertParseFails(() -> strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile", SAXException.class, SecurityException.class); } /** @@ -491,7 +491,7 @@ static void assertSchemaBlocks(final Source xsd) { */ static void assertSchemaBlocksOrDoesNotLeak(final Source xsd) { assertNoLeakOrThrows(() -> { - strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd); + strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd); return ""; }, "Schema compile", SAXException.class, SecurityException.class); } @@ -499,73 +499,73 @@ static void assertSchemaBlocksOrDoesNotLeak(final Source xsd) { /** * Asserts a hardened Schema compilation succeeds. * - *{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; positive control for DOCTYPE-only payloads.
+ *{@link SchemaFactory#newSchema(Source)} via {@link SafeSchemaFactory#newInstance(String)}; positive control for DOCTYPE-only payloads.
*/ static void assertSchemaCompiles(final Source xsd) { - assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); + assertParseSucceeds(() -> strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); } /** * Asserts a hardened Schema compilation completes without throwing. * - *{@link SchemaFactory#newSchema(Source)} via {@link XmlFactories#newSchemaFactory(String)}; use this when the hardening contract guarantees the compile + *
{@link SchemaFactory#newSchema(Source)} via {@link SafeSchemaFactory#newInstance(String)}; use this when the hardening contract guarantees the compile * succeeds but never resolves the external resource (for example, {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skipping the external subset, with the body's * undeclared entity reference dropped per XML 1.0 §4.1).
*/ static void assertSchemaDoesNotLeak(final Source xsd) { - assertParseSucceeds(() -> strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); + assertParseSucceeds(() -> strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), xsd), "Schema compile"); } /** * Asserts a hardened StAX parse of the payload throws. * - *{@link XMLStreamReader} and {@link XMLEventReader} from {@link XmlFactories#newXMLInputFactory()}; both flavors are exercised and either must + *
{@link XMLStreamReader} and {@link XMLEventReader} from {@link SafeXMLInputFactory#newFactory()}; both flavors are exercised and either must * throw.
*/ static void assertStaxBlocks(final String payload) { - assertParseFails(() -> consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream", XMLStreamException.class); - assertParseFails(() -> consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event", XMLStreamException.class); + assertParseFails(() -> consumeStreamReader(SafeXMLInputFactory.newFactory(), payload), "StAX stream", XMLStreamException.class); + assertParseFails(() -> consumeEventReader(SafeXMLInputFactory.newFactory(), payload), "StAX event", XMLStreamException.class); } /** * Asserts a hardened StAX parse (stream and event) either blocks at parse or completes without leaked content. See {@link #assertDomBlocksOrDoesNotLeak(String)}. */ static void assertStaxBlocksOrDoesNotLeak(final String payload) { - assertNoLeakOrThrows(() -> captureStaxStreamText(XmlFactories.newXMLInputFactory(), payload), "StAX stream", XMLStreamException.class); - assertNoLeakOrThrows(() -> captureStaxEventText(XmlFactories.newXMLInputFactory(), payload), "StAX event", XMLStreamException.class); + assertNoLeakOrThrows(() -> captureStaxStreamText(SafeXMLInputFactory.newFactory(), payload), "StAX stream", XMLStreamException.class); + assertNoLeakOrThrows(() -> captureStaxEventText(SafeXMLInputFactory.newFactory(), payload), "StAX event", XMLStreamException.class); } /** * Asserts a hardened StAX parse completes without throwing and without leaked content. * - *{@link XMLStreamReader} and {@link XMLEventReader} from {@link XmlFactories#newXMLInputFactory()}; both flavors are exercised. Use this when the + *
{@link XMLStreamReader} and {@link XMLEventReader} from {@link SafeXMLInputFactory#newFactory()}; both flavors are exercised. Use this when the * hardening guarantee is "the parse succeeds but never resolves the external resource", for example, when the JDK's {@code ignore-external-dtd} property silently * skips the external subset.
*/ static void assertStaxDoesNotLeak(final String payload) { - assertNoLeakStrict(() -> captureStaxStreamText(XmlFactories.newXMLInputFactory(), payload), "StAX stream"); - assertNoLeakStrict(() -> captureStaxEventText(XmlFactories.newXMLInputFactory(), payload), "StAX event"); + assertNoLeakStrict(() -> captureStaxStreamText(SafeXMLInputFactory.newFactory(), payload), "StAX stream"); + assertNoLeakStrict(() -> captureStaxEventText(SafeXMLInputFactory.newFactory(), payload), "StAX event"); } /** * Asserts a hardened StAX parse succeeds. * - *{@link XMLStreamReader} and {@link XMLEventReader} from {@link XmlFactories#newXMLInputFactory()}; positive control for DOCTYPE-only payloads.
+ *{@link XMLStreamReader} and {@link XMLEventReader} from {@link SafeXMLInputFactory#newFactory()}; positive control for DOCTYPE-only payloads.
*/ static void assertStaxParses(final String payload) { - assertParseSucceeds(() -> consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream"); - assertParseSucceeds(() -> consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event"); + assertParseSucceeds(() -> consumeStreamReader(SafeXMLInputFactory.newFactory(), payload), "StAX stream"); + assertParseSucceeds(() -> consumeEventReader(SafeXMLInputFactory.newFactory(), payload), "StAX event"); } /** * Asserts a hardened Templates compile-and-transform throws. * - *{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; either step throwing + *
{@link TransformerFactory#newTemplates(Source)} via {@link SafeTransformerFactory#newInstance()} followed by transform; either step throwing * passes.
*/ static void assertTemplatesBlocks(final Source xslt) { assertParseFails(() -> { - final Templates templates = strictTemplates(XmlFactories.newTransformerFactory(), xslt); + final Templates templates = strictTemplates(SafeTransformerFactory.newInstance(), xslt); // Xalan returns `null` if the template fails if (templates == null) { throw new TransformerException("Transformer factory returned null"); @@ -584,7 +584,7 @@ static void assertTemplatesBlocksOrDoesNotLeak(final Source xslt) { /** * Asserts a hardened Templates compile-and-transform succeeds. * - *{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; positive control for + *
{@link TransformerFactory#newTemplates(Source)} via {@link SafeTransformerFactory#newInstance()} followed by transform; positive control for * DOCTYPE-only payloads.
*/ static void assertTemplatesCompiles(final Source xslt) { @@ -594,7 +594,7 @@ static void assertTemplatesCompiles(final Source xslt) { /** * Asserts a hardened Templates compile-and-transform completes without throwing and without leaked content. * - *{@link TransformerFactory#newTemplates(Source)} via {@link XmlFactories#newTransformerFactory()} followed by transform; use this when the hardening + *
{@link TransformerFactory#newTemplates(Source)} via {@link SafeTransformerFactory#newInstance()} followed by transform; use this when the hardening * contract guarantees the compile and transform succeed but never resolve the external resource.
*/ static void assertTemplatesDoesNotLeak(final Source xslt) { @@ -604,12 +604,12 @@ static void assertTemplatesDoesNotLeak(final Source xslt) { /** * Asserts a hardened identity Transformer of the payload throws. * - *{@link Transformer#transform(Source, javax.xml.transform.Result)} on the identity transformer from {@link XmlFactories#newTransformerFactory()}; only + *
{@link Transformer#transform(Source, javax.xml.transform.Result)} on the identity transformer from {@link SafeTransformerFactory#newInstance()}; only * a thrown exception passes.
*/ static void assertTransformerBlocks(final String payload) { assertParseFails( - () -> strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload), new StreamResult(new StringWriter())), + () -> strictTransformer(SafeTransformerFactory.newInstance()).transform(streamSource(payload), new StreamResult(new StringWriter())), "Transformer", TransformerException.class); } @@ -623,7 +623,7 @@ static void assertTransformerBlocksOrDoesNotLeak(final String payload) { /** * Asserts a hardened identity Transformer completes without throwing and without leaked content. * - *{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link XmlFactories#newTransformerFactory()}; use this when the hardening + *
{@link Transformer#transform(Source, javax.xml.transform.Result)} via {@link SafeTransformerFactory#newInstance()}; use this when the hardening * contract guarantees the transform succeeds but never resolves the external resource.
*/ static void assertTransformerDoesNotLeak(final String payload) { @@ -633,7 +633,7 @@ static void assertTransformerDoesNotLeak(final String payload) { /** * Asserts a hardened identity Transformer succeeds. * - *{@link Transformer#transform(Source, javax.xml.transform.Result)} on the identity transformer from {@link XmlFactories#newTransformerFactory()}; + *
{@link Transformer#transform(Source, javax.xml.transform.Result)} on the identity transformer from {@link SafeTransformerFactory#newInstance()}; * positive control for DOCTYPE-only payloads.
*/ static void assertTransformerTransforms(final String payload) { @@ -643,12 +643,12 @@ static void assertTransformerTransforms(final String payload) { /** * Asserts a hardened Validator validation throws. * - *{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; only a thrown + *
{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link SafeSchemaFactory#newInstance(String)}; only a thrown * exception passes (the schema is benign; the attack lives in the instance document).
*/ static void assertValidatorBlocks(final String xml) { assertParseFails( - () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + () -> strictValidator(strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator", SAXException.class, SecurityException.class); } @@ -659,7 +659,7 @@ static void assertValidatorBlocks(final String xml) { */ static void assertValidatorBlocksOrDoesNotLeak(final String xml) { assertNoLeakOrThrows(() -> { - strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)); + strictValidator(strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)); return ""; }, "Validator", SAXException.class, SecurityException.class, IOException.class); } @@ -667,24 +667,24 @@ static void assertValidatorBlocksOrDoesNotLeak(final String xml) { /** * Asserts a hardened Validator validation completes without throwing. * - *{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; use this when the + *
{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link SafeSchemaFactory#newInstance(String)}; use this when the * hardening contract guarantees the validate succeeds but never resolves the external resource.
*/ static void assertValidatorDoesNotLeak(final String xml) { assertParseSucceeds( - () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + () -> strictValidator(strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator"); } /** * Asserts a hardened Validator validation succeeds. * - *{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory(String)}; positive control + *
{@link Validator#validate(Source)} on a validator from {@link #BENIGN_SCHEMA} compiled via {@link SafeSchemaFactory#newInstance(String)}; positive control * for DOCTYPE-only payloads.
*/ static void assertValidatorValidates(final String xml) { assertParseSucceeds( - () -> strictValidator(strictSchema(XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), + () -> strictValidator(strictSchema(SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI), streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)), "Validator"); } @@ -834,7 +834,7 @@ private static void consumeXmlReader(final XMLReader reader, final String payloa } private static String domParseAndCaptureText(final String payload) throws Exception { - final Document doc = strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)); + final Document doc = strictDocumentBuilder(SafeDocumentBuilderFactory.newInstance()).parse(inputSource(payload)); if (doc.getDocumentElement() == null) { return ""; } @@ -845,7 +845,7 @@ private static String domParseAndCaptureText(final String payload) throws Except private static String identityTransformAndCapture(final String payload) throws TransformerException { final StringWriter sink = new StringWriter(); - strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload), new StreamResult(sink)); + strictTransformer(SafeTransformerFactory.newInstance()).transform(streamSource(payload), new StreamResult(sink)); return sink.toString(); } @@ -1042,7 +1042,7 @@ private static void suppressException(final Executable action) { private static String templatesCompileAndTransform(final Source xslt) throws TransformerException { final StringWriter sink = new StringWriter(); - final Templates templates = strictTemplates(XmlFactories.newTransformerFactory(), xslt); + final Templates templates = strictTemplates(SafeTransformerFactory.newInstance(), xslt); // Xalan returns `null` if the template fails if (templates != null) { strictTransformer(templates).transform(streamSource("The floors are exercised directly: with the property set and no caller delegate, each must throw its hook's exception instead of resolving to empty * content. The property is read at resolution time, so setting it around a single test cannot leak into the rest of the suite.
@@ -40,12 +40,12 @@ class DenyUnresolvedTest { @AfterEach void clearThrowOnUnresolved() { - System.clearProperty(XmlFactories.THROW_ON_UNRESOLVED); + System.clearProperty(HardeningException.THROW_ON_UNRESOLVED); } @BeforeEach void enableThrowOnUnresolved() { - System.setProperty(XmlFactories.THROW_ON_UNRESOLVED, "true"); + System.setProperty(HardeningException.THROW_ON_UNRESOLVED, "true"); } @Test diff --git a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java index e8fe90f3..db7396b3 100644 --- a/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java +++ b/src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java @@ -119,20 +119,20 @@ private static String entityPayload(final String entitySystemId) { } private static XMLInputFactory externalEntityStaxFactory() { - final XMLInputFactory factory = XmlFactories.newXMLInputFactory(); + final XMLInputFactory factory = SafeXMLInputFactory.newFactory(); factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, true); factory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, true); return factory; } private static DocumentBuilder hardenedBuilder() throws Exception { - final DocumentBuilder builder = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder(); + final DocumentBuilder builder = SafeDocumentBuilderFactory.newInstance().newDocumentBuilder(); builder.setErrorHandler(AttackTestSupport.STRICT_REPORTER); return builder; } private static XMLReader hardenedReader() throws Exception { - final XMLReader reader = XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(); + final XMLReader reader = SafeSAXParserFactory.newInstance().newSAXParser().getXMLReader(); reader.setErrorHandler(AttackTestSupport.STRICT_REPORTER); return reader; } @@ -144,7 +144,7 @@ private static XMLReader hardenedReader() throws Exception { * implementation cannot quietly recover from a floor resolution while the test asserts clean completion. */ private static TransformerFactory hardenedTransformerFactory() { - final TransformerFactory factory = XmlFactories.newTransformerFactory(); + final TransformerFactory factory = SafeTransformerFactory.newInstance(); factory.setErrorListener(AttackTestSupport.STRICT_REPORTER); return factory; } @@ -168,7 +168,7 @@ private static LSInput lsInput(final String systemId) { } private static DocumentBuilder xIncludeAwareBuilder() throws Exception { - final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory(); + final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); AttackTestSupport.assumeDoesNotThrow(() -> factory.setXIncludeAware(true)); final DocumentBuilder builder = factory.newDocumentBuilder(); @@ -177,7 +177,7 @@ private static DocumentBuilder xIncludeAwareBuilder() throws Exception { } private static XMLReader xIncludeAwareReader() throws Exception { - final SAXParserFactory factory = XmlFactories.newSAXParserFactory(); + final SAXParserFactory factory = SafeSAXParserFactory.newInstance(); factory.setNamespaceAware(true); AttackTestSupport.assumeDoesNotThrow(() -> factory.setXIncludeAware(true)); final XMLReader reader = factory.newSAXParser().getXMLReader(); @@ -227,7 +227,7 @@ void domResolvesRelativeXIncludeSibling() throws Exception { void saxParseWithHandlerDoesNotBypass() throws Exception { // SAXParser.parse(source, handler) installs the handler as the reader's entity resolver; the handler does not resolve it (returns null), so the // ignore-all floor must still resolve the external entity to empty rather than letting the parser fetch it. - final SAXParser parser = XmlFactories.newSAXParserFactory().newSAXParser(); + final SAXParser parser = SafeSAXParserFactory.newInstance().newSAXParser(); final StringBuilder text = new StringBuilder(); try { parser.parse(AttackTestSupport.inputSource(entityPayload(ALLOWED)), AttackTestSupport.capturingHandler(text)); @@ -276,7 +276,7 @@ void saxResolvesRelativeXIncludeSibling() throws Exception { @Tag("schema") void schemaDeniesUnlisted() { assertParseFails(() -> { - final SchemaFactory factory = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final SchemaFactory factory = SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver((type, namespaceURI, publicId, systemId, baseURI) -> null); factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd")); }, "Schema import", SAXException.class, SecurityException.class); @@ -287,7 +287,7 @@ void schemaDeniesUnlisted() { void schemaFetchesIdentifierOnlyOptIn() { // A non-null return is an opt-in even without content: the implementation fetches the named resource itself, mirroring the entity floor's contract. assertParseSucceeds(() -> { - final SchemaFactory factory = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final SchemaFactory factory = SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver((type, namespaceURI, publicId, systemId, baseURI) -> systemId != null && systemId.endsWith("included.xsd") ? identifierOnlyLsInput(ALLOWED_SCHEMA) : null); factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd")); @@ -299,7 +299,7 @@ void schemaFetchesIdentifierOnlyOptIn() { void schemaResolvesAllowListed() { // with-import.xsd references an element defined only in the imported included.xsd, so it compiles only if the import is resolved. assertParseSucceeds(() -> { - final SchemaFactory factory = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI); + final SchemaFactory factory = SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); factory.setResourceResolver(SCHEMA_ALLOW_LIST); factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd")); }, "Schema import via caller resolver"); @@ -336,7 +336,7 @@ void staxDoesNotLeakUnlisted() throws Exception { @Test @Tag("stax") void staxGetXMLResolverReportsCallerUnwrapped() { - final XMLInputFactory factory = XmlFactories.newXMLInputFactory(); + final XMLInputFactory factory = SafeXMLInputFactory.newFactory(); final XMLResolver caller = (publicID, systemID, baseURI, namespace) -> null; factory.setXMLResolver(caller); assertSame(caller, factory.getXMLResolver(), "getXMLResolver should report the caller's resolver, not the floor wrapper"); diff --git a/src/test/java/org/apache/commons/xml/ResetHardeningTest.java b/src/test/java/org/apache/commons/xml/ResetHardeningTest.java index 687df358..56f9db7d 100644 --- a/src/test/java/org/apache/commons/xml/ResetHardeningTest.java +++ b/src/test/java/org/apache/commons/xml/ResetHardeningTest.java @@ -59,7 +59,7 @@ private static String entityPayload(final String entitySystemId) { @Tag("dom") void documentBuilderResetKeepsEntityResolverFloor() throws Exception { Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES, "platform DOM does not resolve user-defined entities"); - final DocumentBuilder builder = XmlFactories.newDocumentBuilderFactory().newDocumentBuilder(); + final DocumentBuilder builder = SafeDocumentBuilderFactory.newInstance().newDocumentBuilder(); AttackTestSupport.assumeDoesNotThrow(builder::reset); try { final Document doc = builder.parse(AttackTestSupport.inputSource(entityPayload(UNLISTED))); @@ -72,7 +72,7 @@ void documentBuilderResetKeepsEntityResolverFloor() throws Exception { @Test @Tag("sax") void saxParserResetKeepsEntityResolverFloor() throws Exception { - final SAXParser parser = XmlFactories.newSAXParserFactory().newSAXParser(); + final SAXParser parser = SafeSAXParserFactory.newInstance().newSAXParser(); // Materialize the hardened reader before the reset, so a stale cached wrapper would be observable. parser.getXMLReader(); AttackTestSupport.assumeDoesNotThrow(parser::reset); @@ -90,7 +90,7 @@ void saxParserResetKeepsEntityResolverFloor() throws Exception { @Tag("trax") void transformerResetKeepsUriResolverFloor() throws Exception { // with-document.xsl copies document('referenced.xml') into the output at transform time, so a transformer whose floor was stripped leaks the marker. - final Transformer transformer = XmlFactories.newTransformerFactory() + final Transformer transformer = SafeTransformerFactory.newInstance() .newTemplates(AttackTestSupport.resourceSource("with-document.xsl")).newTransformer(); AttackTestSupport.assumeDoesNotThrow(transformer::reset); final StringWriter sink = new StringWriter(); @@ -107,7 +107,7 @@ void transformerResetKeepsUriResolverFloor() throws Exception { void validatorResetKeepsResourceResolverFloor() throws Exception { // A Schema built without sources validates against the instance's xsi:schemaLocation hints, so the resolver floor is the only barrier between the // validator and the external schema fetch. - final Validator validator = XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator(); + final Validator validator = SafeSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema().newValidator(); AttackTestSupport.assumeDoesNotThrow(validator::reset); validator.setErrorHandler(AttackTestSupport.STRICT_REPORTER); // schema-location-instance.xml hints at schema-location.xsd, which declares its root: a validator whose floor was stripped fetches it and validates diff --git a/src/test/java/org/apache/commons/xml/SafeFactoriesTest.java b/src/test/java/org/apache/commons/xml/SafeFactoriesTest.java new file mode 100644 index 00000000..bb7d2548 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/SafeFactoriesTest.java @@ -0,0 +1,179 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import static org.junit.jupiter.api.Assertions.assertEquals; +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.Test; +import org.w3c.dom.Document; +import org.xml.sax.InputSource; + +/** + * Public-API smoke tests for the {@code Safe*} factory classes. + * + *Attack tests live in sibling test classes; this file only verifies that new factories are returned, that they report safe defaults, and that + * a benign document still parses successfully.
+ */ +class SafeFactoriesTest { + + private static final String BENIGN_XML = + "\nThe working W3C XML Schema path is exercised by the whole schema suite; this test covers only the language-selection contract.
*/ @@ -32,7 +32,7 @@ class SchemaFactoryLanguageTest { @Test void unknownSchemaLanguageThrows() { - assertThrows(IllegalArgumentException.class, () -> XmlFactories.newSchemaFactory("urn:example:unknown-schema-language"), + assertThrows(IllegalArgumentException.class, () -> SafeSchemaFactory.newInstance("urn:example:unknown-schema-language"), "an unsupported schema language should surface SchemaFactory.newInstance's IllegalArgumentException"); } } diff --git a/src/test/java/org/apache/commons/xml/SchemaLocationDomTest.java b/src/test/java/org/apache/commons/xml/SchemaLocationDomTest.java index 7cd4c3b1..cf5dc1ce 100644 --- a/src/test/java/org/apache/commons/xml/SchemaLocationDomTest.java +++ b/src/test/java/org/apache/commons/xml/SchemaLocationDomTest.java @@ -87,7 +87,7 @@ private static boolean supportsSchemaLanguage() { @Test void hardenedDoesNotFetchExternalSchema() { assumeTrue(supportsSchemaLanguage(), "parser does not support JAXP 1.2 schema-language XSD validation"); - final DocumentBuilderFactory factory = enableXsdValidation(XmlFactories.newDocumentBuilderFactory()); + final DocumentBuilderFactory factory = enableXsdValidation(SafeDocumentBuilderFactory.newInstance()); // The schemaLocation reference resolves to empty rather than being fetched. Either the empty schema fails the validating parse (acceptable), or the // parse completes but the schema's default leak attribute is never inlined. Either way the marker must not reach the DOM. try { diff --git a/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java b/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java index 127c9208..8a0e89d9 100644 --- a/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java +++ b/src/test/java/org/apache/commons/xml/SchemaLocationPropertyTest.java @@ -82,7 +82,7 @@ private staticUsing {@code jdependency}, the same library {@code maven-shade-plugin}'s {@code minimizeJar} uses, this test computes each entry point's transitive class * closure over the compiled {@code target/classes} and pins it to an expected set. It keeps each hardener from silently regaining a dependency on classes it * should not need (for example a sibling resolver floor or another hardener), so schema builds only on the shared SAX path, TrAX and XPath additionally on the - * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while only the public {@link XmlFactories} entry pulls the whole + * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while the six public {@code Safe*} entry points together pull the whole * library. Update the expected sets deliberately: a change here is a change to what a downstream shade includes.
* *The test reads the compiled {@code .class} files from the code-source location, which only exists on a regular JVM: a native image carries no bytecode (and
@@ -87,15 +87,24 @@ class ShadingFootprintTest {
"HardeningValidatorHandler", "HardeningSchema", "FallbackIgnoreLSResourceResolver");
/**
- * Only the public {@link XmlFactories} entry, which references every hardener, still pulls the whole library; this is its class count.
+ * The six public {@code Safe*} entry points together reference every hardener and pull the whole library; this is their combined class count. One more
+ * than the old single-entry-point count plus the six entry points: {@code SafeSchemaFactory} routes through {@link SchemaHardener}, which the old entry
+ * point bypassed.
*/
- private static final int LIBRARY_CLASS_COUNT = 35;
+ private static final int LIBRARY_CLASS_COUNT = 41;
/**
- * Entry points reported by the {@link #reportFootprint()} diagnostic, most-focused first, ending with the whole library.
+ * The public entry points, one per JAXP factory type; their combined closure is the whole library.
+ */
+ private static final String[] SAFE_ENTRY_POINTS = {"SafeDocumentBuilderFactory", "SafeSAXParserFactory", "SafeSchemaFactory", "SafeTransformerFactory",
+ "SafeXMLInputFactory", "SafeXPathFactory"};
+
+ /**
+ * Entry points reported by the {@link #reportFootprint()} diagnostic, hardeners first, then the public {@code Safe*} entry points.
*/
private static final String[] REPORTED = {"DocumentBuilderHardener", "SAXParserHardener", "StaxHardener", "TransformerHardener", "XPathHardener",
- "SchemaHardener", "XmlFactories"};
+ "SchemaHardener", "SafeDocumentBuilderFactory", "SafeSAXParserFactory", "SafeSchemaFactory", "SafeTransformerFactory", "SafeXMLInputFactory",
+ "SafeXPathFactory"};
private static Clazzpath clazzpath;
private static Path classesDir;
@@ -146,7 +155,7 @@ static void indexCompiledClasses() throws Exception {
*/
@AfterAll
static void reportFootprint() {
- final long library = bytesOf(closureOf("XmlFactories"));
+ final long library = bytesOf(safeClosureUnion());
final StringBuilder report = new StringBuilder("\nShade footprint (uncompressed .class bytes, % of full library):\n");
for (final String entry : REPORTED) {
final Set Each case is exercised in both {@code parse="xml"} and {@code parse="text"} modes, and for both DOM and SAX
@@ -187,7 +187,7 @@ void baselineSaxLeaksParseXml() throws Exception {
void hardenedDomBlocksParseText() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_TEXT, "text"));
- final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final Document doc = factory.newDocumentBuilder().parse(input);
@@ -201,7 +201,7 @@ void hardenedDomBlocksParseText() throws Exception {
void hardenedDomBlocksParseXml() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
- final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
assertThrows(SAXException.class, () -> {
@@ -215,7 +215,7 @@ void hardenedDomBlocksParseXml() throws Exception {
void hardenedDomNullResolverDoesNotLeak() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
- final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final DocumentBuilder builder = factory.newDocumentBuilder();
@@ -229,7 +229,7 @@ void hardenedDomNullResolverDoesNotLeak() throws Exception {
void hardenedDomWithAllowListResolvesParseText() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_TEXT, "text"));
- final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final DocumentBuilder builder = factory.newDocumentBuilder();
@@ -248,7 +248,7 @@ void hardenedDomWithAllowListResolvesParseText() throws Exception {
void hardenedDomWithAllowListResolvesParseXml() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
- final DocumentBuilderFactory factory = XmlFactories.newDocumentBuilderFactory();
+ final DocumentBuilderFactory factory = SafeDocumentBuilderFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final DocumentBuilder builder = factory.newDocumentBuilder();
@@ -263,7 +263,7 @@ void hardenedDomWithAllowListResolvesParseXml() throws Exception {
void hardenedSaxBlocksParseText() throws Exception {
final String input = xiIncludeXml(REFERENCED_TEXT, "text");
- final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ final SAXParserFactory factory = SafeSAXParserFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final String captured = captureCharacters(factory.newSAXParser().getXMLReader(), input);
@@ -276,7 +276,7 @@ void hardenedSaxBlocksParseText() throws Exception {
void hardenedSaxBlocksParseXml() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
- final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ final SAXParserFactory factory = SafeSAXParserFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
assertThrows(SAXException.class, () -> {
@@ -290,7 +290,7 @@ void hardenedSaxBlocksParseXml() throws Exception {
void hardenedSaxNullResolverDoesNotLeak() throws Exception {
final InputSource input = inputSource(xiIncludeXml(REFERENCED_XML, "xml"));
- final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ final SAXParserFactory factory = SafeSAXParserFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final XMLReader reader = factory.newSAXParser().getXMLReader();
@@ -304,7 +304,7 @@ void hardenedSaxNullResolverDoesNotLeak() throws Exception {
void hardenedSaxWithAllowListResolvesParseText() throws Exception {
final String input = xiIncludeXml(REFERENCED_TEXT, "text");
- final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ final SAXParserFactory factory = SafeSAXParserFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final XMLReader reader = factory.newSAXParser().getXMLReader();
@@ -319,7 +319,7 @@ void hardenedSaxWithAllowListResolvesParseText() throws Exception {
void hardenedSaxWithAllowListResolvesParseXml() throws Exception {
final String input = xiIncludeXml(REFERENCED_XML, "xml");
- final SAXParserFactory factory = XmlFactories.newSAXParserFactory();
+ final SAXParserFactory factory = SafeSAXParserFactory.newInstance();
factory.setNamespaceAware(true);
assumeXIncludeAware(factory);
final XMLReader reader = factory.newSAXParser().getXMLReader();
diff --git a/src/test/java/org/apache/commons/xml/XMLFilterParseStringTest.java b/src/test/java/org/apache/commons/xml/XMLFilterParseStringTest.java
index e7a2a98f..a8dc45ec 100644
--- a/src/test/java/org/apache/commons/xml/XMLFilterParseStringTest.java
+++ b/src/test/java/org/apache/commons/xml/XMLFilterParseStringTest.java
@@ -53,7 +53,7 @@ private static String entityPayload() {
@Test
void hardenedFilterParseStringDoesNotLeakExternalEntity(@TempDir final Path tmpDir) throws Exception {
- final SAXTransformerFactory factory = (SAXTransformerFactory) XmlFactories.newTransformerFactory();
+ final SAXTransformerFactory factory = (SAXTransformerFactory) SafeTransformerFactory.newInstance();
final Templates templates = factory.newTemplates(new StreamSource(new StringReader(IDENTITY_XSLT)));
final XMLFilter filter = factory.newXMLFilter(templates);
final Path tmp = Files.createTempFile(tmpDir, "xmlfilter", ".xml");
diff --git a/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java b/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java
index e28acd34..8161f0ae 100644
--- a/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java
+++ b/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java
@@ -52,14 +52,14 @@ private static String entityPayload() {
void hardenedXPathEvaluateDoesNotLeak() throws Exception {
// Deterministic on every engine: the entity is declared in the internal subset and the floor resolves only its
// external content — to empty replacement text — so the pre-parse completes and the reference expands to nothing.
- final String result = XmlFactories.newXPathFactory().newXPath().evaluate(EXPRESSION, AttackTestSupport.inputSource(entityPayload()));
+ final String result = SafeXPathFactory.newInstance().newXPath().evaluate(EXPRESSION, AttackTestSupport.inputSource(entityPayload()));
assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), "external entity leaked into the XPath result: " + result);
}
@Test
void hardenedXPathEvaluatesPlainDocument() throws Exception {
// Positive control: the hardened pre-parse still evaluates an entity-free document end to end.
- final String result = XmlFactories.newXPathFactory().newXPath().evaluate(EXPRESSION,
+ final String result = SafeXPathFactory.newInstance().newXPath().evaluate(EXPRESSION,
AttackTestSupport.inputSource(AttackTestSupport.xmlBody("plain text")));
assertEquals("plain text", result, "hardened XPath should evaluate a plain document");
}
@@ -67,7 +67,7 @@ void hardenedXPathEvaluatesPlainDocument() throws Exception {
@Test
void hardenedXPathExpressionEvaluateDoesNotLeak() throws Exception {
// Same declared-entity outcome as above on the compiled-expression entry point.
- final String result = XmlFactories.newXPathFactory().newXPath().compile(EXPRESSION).evaluate(AttackTestSupport.inputSource(entityPayload()));
+ final String result = SafeXPathFactory.newInstance().newXPath().compile(EXPRESSION).evaluate(AttackTestSupport.inputSource(entityPayload()));
assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), "external entity leaked into the compiled XPath result: " + result);
}
diff --git a/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java b/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java
deleted file mode 100644
index e61d64bd..00000000
--- a/src/test/java/org/apache/commons/xml/XmlFactoriesTest.java
+++ /dev/null
@@ -1,122 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package org.apache.commons.xml;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-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.assertTrue;
-
-import java.io.StringReader;
-
-import javax.xml.XMLConstants;
-import javax.xml.parsers.DocumentBuilderFactory;
-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.Test;
-import org.w3c.dom.Document;
-import org.xml.sax.InputSource;
-
-/**
- * Public-API smoke tests for {@link XmlFactories}.
- *
- * Attack tests live in the {@code attacks} sub-package; this file only verifies that new factories are returned, that they report safe defaults, and that
- * a benign document still parses successfully. The {@code Safe*} factory classes ship as three progressively richer versions: the base release-8 classes, a {@code META-INF/versions/9} layer adding the
+ * Java 9 JAXP factory methods, and a {@code META-INF/versions/13} layer adding the {@code newNSInstance} family on the two parser factories. Surefire runs
+ * against the exploded {@code target/classes} directory, where the JVM never applies Multi-Release selection, so only a test against the built jar can verify
+ * that a versioned class is actually picked up and that its added methods return hardened factories. The versioned layers are compiled only when the build JDK supports them (profiles {@code java9-multi-release} and {@code java13-multi-release}), so each
+ * group of assertions is gated on its layer being present in the jar rather than on the JDK version. The class loader parents on the platform loader on purpose: failsafe also puts the exploded {@code target/classes} on the application class path, and
+ * a default-parented loader would resolve the classes from there, where Multi-Release selection never applies.