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. + + + java9-multi-release + + [9,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-java9 + compile + + compile + + + 9 + true + + ${project.basedir}/src/main/java9 + + + + + + + + + + java13-multi-release + + [13,) + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + compile-java13 + compile + + compile + + + 13 + true + + ${project.basedir}/src/main/java13 + + + + + + + + This is the first release. - Add secure-by-default JAXP factory creation via XmlFactories, donated from the copernik-xml-factory project (https://github.com/copernik-eu/copernik-xml-factory) and covering the stock JDK, Android, Apache Xalan, Apache Xerces, Woodstox, and Saxon-HE. + Add secure-by-default JAXP factory creation via one Safe factory class per JAXP factory type (SafeDocumentBuilderFactory and siblings), donated from the copernik-xml-factory project (https://github.com/copernik-eu/copernik-xml-factory) and covering the stock JDK, Android, Apache Xalan, Apache Xerces, Woodstox, and Saxon-HE. + Mirror the JAXP factory methods introduced in Java 9 and 13 (newDefaultInstance, newDefaultFactory, and the newNSInstance family) through a Multi-Release jar. Install a non-removable resolver floor on every resolver channel (EntityResolver, LSResourceResolver, URIResolver, and XMLResolver), routing caller-supplied resolvers through it as allow-lists. Harden the SAXTransformerFactory extension surface (TransformerHandler, TemplatesHandler, and XMLFilter) and TransformerFactory.getAssociatedStylesheet. Document the threat model on the project site, including the denied-fetch contract and the supported runtime floor (OpenJDK 8 and Android API 33 or later). diff --git a/src/main/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2.java b/src/main/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2.java index 0b7b774a..bda79e2f 100644 --- a/src/main/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2.java +++ b/src/main/java/org/apache/commons/xml/FallbackIgnoreEntityResolver2.java @@ -109,7 +109,7 @@ final EntityResolver getDelegate() { * @param baseURI The base URI for relative resolution, or {@code null}. * @param systemId The system identifier of the unresolved entity. * @return An empty {@link InputSource} carrying the requested identifiers. - * @throws SAXException when {@value XmlFactories#THROW_ON_UNRESOLVED} is set: unresolved references are rejected instead of resolved to empty. + * @throws SAXException when {@value HardeningException#THROW_ON_UNRESOLVED} is set: unresolved references are rejected instead of resolved to empty. * @throws IOException never by the default implementation. */ protected InputSource onUnresolved(final String name, final String publicId, final String baseURI, final String systemId) throws SAXException, IOException { diff --git a/src/main/java/org/apache/commons/xml/HardeningException.java b/src/main/java/org/apache/commons/xml/HardeningException.java index 4dd123c7..fdda6bac 100644 --- a/src/main/java/org/apache/commons/xml/HardeningException.java +++ b/src/main/java/org/apache/commons/xml/HardeningException.java @@ -33,6 +33,15 @@ */ final class HardeningException extends IllegalStateException { + /** + * 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"; + private static final long serialVersionUID = 1L; /** @@ -47,7 +56,7 @@ final class HardeningException extends IllegalStateException { */ static String forbidden(final String type, final String namespace, final String publicId, final String systemId, final String baseURI) { return String.format("External resource fetch forbidden by %s: type=%s, namespace=%s, publicId=%s, systemId=%s, baseURI=%s", - XmlFactories.THROW_ON_UNRESOLVED, type, namespace, publicId, systemId, baseURI); + THROW_ON_UNRESOLVED, type, namespace, publicId, systemId, baseURI); } /** @@ -66,12 +75,12 @@ static HardeningException settingFailed(final String kind, final String name, fi /** * Whether unresolved external references must be rejected instead of resolved to empty content. * - *

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.

* - * @return {@code true} when the {@value XmlFactories#THROW_ON_UNRESOLVED} system property is set. + * @return {@code true} when the {@value #THROW_ON_UNRESOLVED} system property is set. */ static boolean throwOnUnresolved() { - return Boolean.getBoolean(XmlFactories.THROW_ON_UNRESOLVED); + return Boolean.getBoolean(THROW_ON_UNRESOLVED); } HardeningException(final String message, final Throwable cause) { diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index c55fe913..38b907d0 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -34,7 +34,7 @@ /** * Capability-driven hardening wrapper for any {@link SchemaFactory} on the classpath, the same recipe for every implementation. It is the entry point reached - * by {@link XmlFactories#newSchemaFactory(String)}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the + * by {@link SafeSchemaFactory#newInstance(String)}; there is no per-implementation branching, no {@code FEATURE_SECURE_PROCESSING} and no limit configuration on the * factory itself. * *

Three layers cooperate:

diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index 948c5b90..f8eea8b7 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -50,7 +50,7 @@ * *

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.

+ * Wrapping the factory and rewriting the Source upstream guarantees the parse runs through a {@link SafeSAXParserFactory}-hardened reader instead.

* *

Three layers cooperate:

*
    @@ -69,7 +69,7 @@ *

    Caveats

    * */ diff --git a/src/main/java/org/apache/commons/xml/SafeDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/SafeDocumentBuilderFactory.java new file mode 100644 index 00000000..a2fc72ce --- /dev/null +++ b/src/main/java/org/apache/commons/xml/SafeDocumentBuilderFactory.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.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)); + } + + 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. *
  1. {@link HardeningTransformerFactory}: required. Both implementations fall back to {@code SAXParserFactory.newInstance()} to parse a - * stylesheet or source document that does not carry its own reader, and only set FSP on it; wrapping the factory rewrites every {@link Source} through an - * {@link XmlFactories}-hardened reader instead.
  2. + * stylesheet or source document that does not carry its own reader, and only set FSP on it; wrapping the factory rewrites every {@link Source} through a + * {@link SafeSAXParserFactory}-hardened reader instead. * */ final class TransformerHardener { diff --git a/src/main/java/org/apache/commons/xml/XmlFactories.java b/src/main/java/org/apache/commons/xml/XmlFactories.java deleted file mode 100644 index 0330be06..00000000 --- a/src/main/java/org/apache/commons/xml/XmlFactories.java +++ /dev/null @@ -1,213 +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 javax.xml.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -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.validation.SchemaFactoryConfigurationError; -import javax.xml.xpath.XPathFactory; - -/** - * Entry point for obtaining hardened JAXP factories. - * - *

    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.

    - * - *

    Hardening guarantees

    - * - *

    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.

    - * - *

    Caller-supplied URIs

    - * - *

    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.

    - * - *

    Thread safety

    - * - *

    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.

    + * + *

    Hardening guarantees

    + * + *

    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.

    + * + *

    Caller-supplied URIs

    + * + *

    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.

    + * + *

    Thread safety

    + * + *

    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:

    * *