Skip to content
Merged
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
32 changes: 31 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,10 @@ limitations under the License.
</pluginManagement>
<plugins>
<!--
Run the test suite seven times, each with a distinct JAXP combination:
Run the test suite eight times, each with a distinct JAXP combination:

- test-jdk-xerces: stock JDK TrAX and XPath over Apache Xerces SAX/DOM
(the only cell where jdk.xml.overrideDefaultParser decides which parser rewrites sources)
- test-saxon: Saxon-HE (TrAX, XPath) over stock JDK SAX
- test-saxon-xerces: Saxon-HE (TrAX, XPath) over stock JDK SAX or Apache Xerces SAX
(Saxon prefers the JDK SAX provider but may resolve to Xerces)
Expand Down Expand Up @@ -322,6 +324,30 @@ limitations under the License.
</classpathDependencyExcludes>
</configuration>
</execution>
<execution>
<id>test-jdk-xerces</id>
<goals>
<goal>test</goal>
</goals>
<configuration>
<reportsDirectory>${project.build.directory}/surefire-reports/jdk-xerces</reportsDirectory>
<!-- The only cell pairing the JDK TrAX and XPath implementations with a third-party ServiceLoader parser: Xerces resolves the
SAXParserFactory/DocumentBuilderFactory lookups (and the SchemaFactory one, which is why the schema-group FODP tests pin the JDK
implementation via newDefaultInstance), so jdk.xml.overrideDefaultParser genuinely decides which parser performs the hardened
source rewrites. -->
<groups>trax,xpath,schema</groups>
<additionalClasspathDependencies>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>${commons.xerces.version}</version>
</dependency>
</additionalClasspathDependencies>
<classpathDependencyExcludes>
<exclude>net.sf.saxon:Saxon-HE</exclude>
</classpathDependencyExcludes>
</configuration>
</execution>
</executions>
</plugin>
<!-- Do not publish a test-jar: the test classes are not part of the consumer contract. -->
Expand Down Expand Up @@ -548,6 +574,10 @@ limitations under the License.
<id>test-xerces</id>
<phase>none</phase>
</execution>
<execution>
<id>test-jdk-xerces</id>
<phase>none</phase>
</execution>
</executions>
</plugin>
<plugin>
Expand Down
1 change: 1 addition & 0 deletions src/changes/changes.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ The <action> type attribute can be add, update, fix, or remove.
<action type="add" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Mirror on each factory class every JAXP static factory method, including the Java 9 newDefaultInstance and Java 13 newNSInstance families, all usable on Java 8.</action>
<!-- FIX -->
<action type="fix" dev="ppkarwasz" due-to="Ta Duc Thien, Piotr P. Karwasz, Gary Gregory" issue="COMMONSXML-10">Block XInclude (xi:include) href resolution by default, since the JAXP external-access properties do not govern it.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Honor jdk.xml.overrideDefaultParser on TrAX, XPath and schema factories that recognize it.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Restore the hardened configuration when a factory or parser is reset() instead of reverting to the implementation defaults.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Parse a Source opted in by a caller-supplied URIResolver using a hardened parser.</action>
<action type="fix" dev="ppkarwasz" due-to="Piotr P. Karwasz, Gary Gregory">Harden the document parse behind the InputSource-taking XPath evaluation entry points.</action>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.apache.commons.xml;

import java.util.function.BooleanSupplier;
import java.util.function.Supplier;

import javax.xml.parsers.DocumentBuilderFactory;
Expand Down Expand Up @@ -85,15 +86,23 @@ private static Document newEmptyDocument() {
*/
private final Supplier<Source> emptySource;

/**
* Whether the opted-in rewrite should use the pluggable parser lookup instead of the platform's built-in parser; read per resolution so the factory-level floor tracks a later
* {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} toggle.
*/
private final BooleanSupplier overrideDefaultParser;

/**
* Constructs a new resolver.
*
* @param delegate the resolver to delegate resolution to; may be {@code null}.
* @param emptySource the empty-{@link Source} supplier for the ignore outcome, or {@code null} for the default empty DOM document.
* @param delegate the resolver to delegate resolution to; may be {@code null}.
* @param emptySource the empty-{@link Source} supplier for the ignore outcome, or {@code null} for the default empty DOM document.
* @param overrideDefaultParser whether the opted-in rewrite should use the pluggable parser lookup instead of the platform's built-in parser, read at each resolution.
*/
FallbackIgnoreURIResolver(final URIResolver delegate, final Supplier<Source> emptySource) {
FallbackIgnoreURIResolver(final URIResolver delegate, final Supplier<Source> emptySource, final BooleanSupplier overrideDefaultParser) {
this.delegate = delegate;
this.emptySource = emptySource != null ? emptySource : () -> new DOMSource(EMPTY_DOCUMENT);
this.overrideDefaultParser = overrideDefaultParser;
}

/**
Expand All @@ -116,7 +125,7 @@ public Source resolve(final String href, final String base) throws TransformerEx
final Source resolved = delegate != null ? delegate.resolve(href, base) : null;
if (resolved != null) {
// The implementation parses the opted-in handle with an internal reader at its own defaults; the rewrite hands it a hardened reader instead.
return HardeningSAXParserFactory.harden(resolved);
return HardeningSAXParserFactory.harden(resolved, overrideDefaultParser.getAsBoolean());
}
if (HardeningException.throwOnUnresolved()) {
throw new TransformerException(HardeningException.forbidden("uri", null, null, href, base));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ public final class HardeningDocumentBuilderFactory {

/** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */
private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl";
/** System property naming the {@link DocumentBuilderFactory} implementation, the JDK's own mechanism for reconfiguring the default parser. */
private static final String DOM_FACTORY_ID = "javax.xml.parsers.DocumentBuilderFactory";
/** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
private static final String JDK_DOCUMENT_BUILDER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl";

Expand Down Expand Up @@ -184,6 +186,24 @@ public static DocumentBuilderFactory newNSInstance() {
return makeNSAware(newInstance());
}

/**
* Returns the hardened, namespace-aware factory the Source-rewriting wrappers parse with.
* <p>
* While {@code overrideDefaultParser} is {@code false} the factory is the JDK's "default parser" factory, determined the way the JDK itself determines it: the built-in
* implementation, unless the {@value #DOM_FACTORY_ID} system property is set — that property is the JDK's own mechanism for
* reconfiguring the default parser, so it is honored through the standard lookup rather than bypassed.
* </p>
*
* @param overrideDefaultParser whether {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} on the originating factory asks to override the JDK's default parser.
* @return A hardened, namespace-aware factory.
* @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation.
* @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the
* implementation is not available or cannot be instantiated.
*/
static DocumentBuilderFactory newNSInstance(final boolean overrideDefaultParser) {
return overrideDefaultParser || System.getProperty(DOM_FACTORY_ID) != null ? newNSInstance() : newDefaultNSInstance();
}

/**
* Returns a new, hardened, namespace-aware {@link DocumentBuilderFactory} of the given implementation class, enabling namespace awareness on
* {@link #newInstance(String, ClassLoader)}, the behavior {@code DocumentBuilderFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ public final class HardeningSAXParserFactory {
/** Class name of the JDK's built-in default implementation, the Java 8 fallback for {@link #newDefaultInstance()}. */
private static final String JDK_SAX_PARSER_FACTORY = "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl";

/**
* The JDK feature governing whether an implementation's internal parser lookup may resolve a third-party parser. The hardening wrappers parse every source
* themselves, so instead of configuring the implementation the TrAX, XPath and schema wrappers read this feature and pick the rewrite parser accordingly.
*/
static final String OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser";

/** System property naming the {@link SAXParserFactory} implementation, the JDK's own mechanism for reconfiguring the default parser. */
private static final String SAX_FACTORY_ID = "javax.xml.parsers.SAXParserFactory";

private static final MethodHandle NEW_DEFAULT_INSTANCE = MethodHandleFactory.findStatic(SAXParserFactory.class, "newDefaultInstance",
MethodType.methodType(SAXParserFactory.class));

Expand Down Expand Up @@ -105,16 +114,17 @@ static SAXParserFactory harden(final SAXParserFactory factory) {
* as-is. Used by the TrAX and schema wrappers to route every source they parse through the SAX hardening path.
* </p>
*
* @param source the source to harden; never {@code null}.
* @param source the source to harden; never {@code null}.
* @param overrideDefaultParser whether {@value #OVERRIDE_DEFAULT_PARSER} on the originating factory asks to override the JDK's default parser.
* @return a hardened source.
* @throws TransformerConfigurationException if a hardened reader cannot be obtained.
* @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.
*/
static Source harden(final Source source) throws TransformerConfigurationException {
static Source harden(final Source source, final boolean overrideDefaultParser) throws TransformerConfigurationException {
if (source instanceof StreamSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() == null) {
final InputSource inputSource = SAXSource.sourceToInputSource(source);
return inputSource == null ? source : new SAXSource(newHardenedReader(), inputSource);
return inputSource == null ? source : new SAXSource(newHardenedReader(overrideDefaultParser), inputSource);
}
return source;
}
Expand Down Expand Up @@ -200,16 +210,18 @@ public static SAXParserFactory newDefaultNSInstance() {
}

/**
* Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with.
* Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX, XPath and schema wrappers to parse sources with, from the factory
* {@link #newNSInstance(boolean)} selects.
*
* @param overrideDefaultParser whether {@value #OVERRIDE_DEFAULT_PARSER} on the originating factory asks to override the JDK's default parser.
* @return a hardened reader.
* @throws TransformerConfigurationException if a hardened reader cannot be obtained.
* @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.
*/
static XMLReader newHardenedReader() throws TransformerConfigurationException {
static XMLReader newHardenedReader(final boolean overrideDefaultParser) throws TransformerConfigurationException {
try {
return newNSInstance().newSAXParser().getXMLReader();
return newNSInstance(overrideDefaultParser).newSAXParser().getXMLReader();
} catch (final ParserConfigurationException | SAXException e) {
throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e);
}
Expand Down Expand Up @@ -253,6 +265,24 @@ public static SAXParserFactory newNSInstance() {
return makeNSAware(newInstance());
}

/**
* Returns the hardened, namespace-aware factory the Source-rewriting wrappers parse with.
* <p>
* While {@code overrideDefaultParser} is {@code false} the factory is the JDK's "default parser" factory, determined the way the JDK itself determines it: the built-in parser,
* unless the {@value #SAX_FACTORY_ID} system property is set — that property is the JDK's own mechanism for reconfiguring the default
* parser, so it is honored through the standard lookup rather than bypassed.
* </p>
*
* @param overrideDefaultParser whether {@value #OVERRIDE_DEFAULT_PARSER} on the originating factory asks to override the JDK's default parser.
* @return A hardened, namespace-aware factory.
* @throws IllegalStateException Thrown if a required hardening setting cannot be applied to the underlying implementation.
* @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service configuration error} or if the
* implementation is not available or cannot be instantiated.
*/
static SAXParserFactory newNSInstance(final boolean overrideDefaultParser) {
return overrideDefaultParser || System.getProperty(SAX_FACTORY_ID) != null ? newNSInstance() : newDefaultNSInstance();
}

/**
* Returns a new, hardened, namespace-aware {@link SAXParserFactory} of the given implementation class, enabling namespace awareness on
* {@link #newInstance(String, ClassLoader)}, the behavior {@code SAXParserFactory.newNSInstance(String, ClassLoader)} (Java 13 or later) is specified to have.
Expand Down
15 changes: 11 additions & 4 deletions src/main/java/org/apache/commons/xml/HardeningSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,27 +25,34 @@

/**
* {@link Schema} wrapper that hardens every {@link Validator} and {@link ValidatorHandler} the inner Schema produces: each {@link Validator} is wrapped in
* {@link HardeningValidator} (which rewrites the Source through {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source)} and installs the resolver
* {@link HardeningValidator} (which rewrites the Source through {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source, boolean)} and installs the resolver
* floor), and each {@link ValidatorHandler} is wrapped in a {@link HardeningValidatorHandler} that keeps the same ignore-all resolver floor so
* {@code xsi:schemaLocation} is not resolved during SAX-driven validation.
*/
final class HardeningSchema extends Schema {

private final Schema delegate;

/**
* Snapshot of the factory's {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome, carried onto every produced Validator.
*/
final boolean overrideDefaultParser;

/**
* Constructs a new instance.
*
* @param delegate the delegate to wrap; must not be {@code null}.
* @param delegate the delegate to wrap; must not be {@code null}.
* @param overrideDefaultParser whether the produced Validators' source rewrites should use the pluggable parser lookup instead of the platform's built-in parser.
* @throws NullPointerException if {@code delegate} is {@code null}.
*/
HardeningSchema(final Schema delegate) {
HardeningSchema(final Schema delegate, final boolean overrideDefaultParser) {
this.delegate = Objects.requireNonNull(delegate, "delegate");
this.overrideDefaultParser = overrideDefaultParser;
}

@Override
public Validator newValidator() {
return new HardeningValidator(delegate.newValidator());
return new HardeningValidator(delegate.newValidator(), overrideDefaultParser);
}

@Override
Expand Down
Loading
Loading