Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,73 @@ limitations under the License.
</plugins>
</reporting>
<profiles>
<!--
Multi-Release jar layers: the `src/main/java9` and `src/main/java13` roots hold full copies of the Safe* factory
classes extended with the JAXP factory methods introduced in those Java versions, compiled into
`META-INF/versions/{9,13}` of the regular output directory. Each layer needs a matching javac, so on older JDKs
the corresponding profile stays inactive and the jar simply ships without that layer. The `Multi-Release`
manifest attribute is added by moditect (commons-parent `java-9-up` profile) at package time.
-->
<profile>
<id>java9-multi-release</id>
<activation>
<jdk>[9,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java9</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>9</release>
<multiReleaseOutput>true</multiReleaseOutput>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java9</compileSourceRoot>
</compileSourceRoots>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<profile>
<id>java13-multi-release</id>
<activation>
<jdk>[13,)</jdk>
</activation>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<executions>
<execution>
<id>compile-java13</id>
<phase>compile</phase>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<release>13</release>
<multiReleaseOutput>true</multiReleaseOutput>
<compileSourceRoots>
<compileSourceRoot>${project.basedir}/src/main/java13</compileSourceRoot>
</compileSourceRoots>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
<!--
Regression guard for the generated OSGi and JPMS descriptors.
The JPMS test requires JDK 9+
Expand Down
3 changes: 2 additions & 1 deletion src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ The <action> type attribute can be add, update, fix, or remove.
<!-- SPECIAL -->
<action type="add" dev="ggregory" due-to="Piotr P. Karwasz, Gary Gregory">This is the first release.</action>
<!-- ADD -->
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">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.</action>
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">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.</action>
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Mirror the JAXP factory methods introduced in Java 9 and 13 (newDefaultInstance, newDefaultFactory, and the newNSInstance family) through a Multi-Release jar.</action>
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory" issue="COMMONSXML-9">Install a non-removable resolver floor on every resolver channel (EntityResolver, LSResourceResolver, URIResolver, and XMLResolver), routing caller-supplied resolvers through it as allow-lists.</action>
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Harden the SAXTransformerFactory extension surface (TransformerHandler, TemplatesHandler, and XMLFilter) and TransformerFactory.getAssociatedStylesheet.</action>
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Jarek Potiuk, Gary Gregory">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).</action>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/org/apache/commons/xml/HardeningException.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* </p>
*/
static final String THROW_ON_UNRESOLVED = "org.apache.commons.xml.throwOnUnresolved";

private static final long serialVersionUID = 1L;

/**
Expand All @@ -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);
}

/**
Expand All @@ -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.
*
* <p>Read per resolution, so the {@value XmlFactories#THROW_ON_UNRESOLVED} system property also toggles factories that already exist.</p>
* <p>Read per resolution, so the {@value #THROW_ON_UNRESOLVED} system property also toggles factories that already exist.</p>
*
* @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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>Three layers cooperate:</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
*
* <p>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.</p>
* Wrapping the factory and rewriting the Source upstream guarantees the parse runs through a {@link SafeSAXParserFactory}-hardened reader instead.</p>
*
* <p>Three layers cooperate:</p>
* <ol>
Expand All @@ -69,7 +69,7 @@
* <h2>Caveats</h2>
* <ul>
* <li>A {@link SAXSource} that carries its own {@link XMLReader} is trusted as-is: the caller is expected to supply a hardened reader (via
* {@link XmlFactories#newSAXParserFactory()}) in that case. The same applies to the SAX events a caller feeds into a handler, and to a parent reader a
* {@link SafeSAXParserFactory#newInstance()}) in that case. The same applies to the SAX events a caller feeds into a handler, and to a parent reader a
* caller sets on a returned {@link XMLFilter}.</li>
* </ul>
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*/
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
}
}
69 changes: 69 additions & 0 deletions src/main/java/org/apache/commons/xml/SafeSAXParserFactory.java
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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}.</p>
*
* <p>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.</p>
*
* <p>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.</p>
*/
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
}
}
Loading
Loading