diff --git a/pom.xml b/pom.xml index 389972fe..984a6a17 100644 --- a/pom.xml +++ b/pom.xml @@ -146,8 +146,10 @@ limitations under the License. + trax,xpath,schema + + + xerces + xercesImpl + ${commons.xerces.version} + + + + net.sf.saxon:Saxon-HE + + + @@ -548,6 +574,10 @@ limitations under the License. test-xerces none + + test-jdk-xerces + none + diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 769d8635..943f1302 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -43,6 +43,7 @@ The type attribute can be add, update, fix, or remove. Mirror on each factory class every JAXP static factory method, including the Java 9 newDefaultInstance and Java 13 newNSInstance families, all usable on Java 8. Block XInclude (xi:include) href resolution by default, since the JAXP external-access properties do not govern it. + Honor jdk.xml.overrideDefaultParser on TrAX, XPath and schema factories that recognize it. Restore the hardened configuration when a factory or parser is reset() instead of reverting to the implementation defaults. Parse a Source opted in by a caller-supplied URIResolver using a hardened parser. Harden the document parse behind the InputSource-taking XPath evaluation entry points. diff --git a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java index cf3e920c..0b6c781c 100644 --- a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java +++ b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java @@ -17,6 +17,7 @@ package org.apache.commons.xml; +import java.util.function.BooleanSupplier; import java.util.function.Supplier; import javax.xml.parsers.DocumentBuilderFactory; @@ -85,15 +86,23 @@ private static Document newEmptyDocument() { */ private final Supplier 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 emptySource) { + FallbackIgnoreURIResolver(final URIResolver delegate, final Supplier emptySource, final BooleanSupplier overrideDefaultParser) { this.delegate = delegate; this.emptySource = emptySource != null ? emptySource : () -> new DOMSource(EMPTY_DOCUMENT); + this.overrideDefaultParser = overrideDefaultParser; } /** @@ -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)); diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index d9cc8063..2421305f 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java @@ -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"; @@ -184,6 +186,24 @@ public static DocumentBuilderFactory newNSInstance() { return makeNSAware(newInstance()); } + /** + * Returns the hardened, namespace-aware factory the Source-rewriting wrappers parse with. + *

+ * 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. + *

+ * + * @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 diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index 5e6a84f4..11a8034e 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -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)); @@ -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. *

* - * @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; } @@ -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); } @@ -253,6 +265,24 @@ public static SAXParserFactory newNSInstance() { return makeNSAware(newInstance()); } + /** + * Returns the hardened, namespace-aware factory the Source-rewriting wrappers parse with. + *

+ * 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. + *

+ * + * @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. diff --git a/src/main/java/org/apache/commons/xml/HardeningSchema.java b/src/main/java/org/apache/commons/xml/HardeningSchema.java index e92374c1..19385d95 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchema.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchema.java @@ -25,7 +25,7 @@ /** * {@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. */ @@ -33,19 +33,26 @@ 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 diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index c97cc484..8732bdbd 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -70,7 +70,7 @@ public final class HardeningSchemaFactory { *

Unlike the other factory types there is no per-implementation branching and no feature or limit configuration on the factory itself: schema compilation * and validation reach external resources only through the resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on * every implementation. The reader used to parse schema and instance documents is hardened separately, through - * {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source)}.

+ * {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source, boolean)}.

* * @param factory the factory to harden; never {@code null}. * @return a hardened factory. @@ -150,14 +150,14 @@ private HardeningSchemaFactory() { *
    *
  1. {@link HardeningSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking * {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point - * through {@link HardeningSAXParserFactory#harden(Source)}.
  2. + * through {@link HardeningSAXParserFactory#harden(Source, boolean)}. *
  3. {@link HardeningSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking * {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.
  4. *
  5. {@link HardeningValidator} rewrites the Source on every {@link Validator#validate(Source)} call.
  6. *
* *

- * The hardened reader supplied by {@link HardeningSAXParserFactory#harden(Source)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a + * The hardened reader supplied by {@link HardeningSAXParserFactory#harden(Source, boolean)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. The JAXP 1.5 * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the resolver floor already blocks the same fetches on every implementation, and the JDK 8 * {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant the access. The floor is a non-removable @@ -170,7 +170,7 @@ private HardeningSchemaFactory() { private static final class Wrapper extends SchemaFactory { /** - * Hardens every schema source through {@link HardeningSAXParserFactory#harden(Source)}. + * Hardens every schema source through {@link HardeningSAXParserFactory#harden(Source, boolean)}. * * @param schemas the schema sources to harden; must not be {@code null}. * @return a new array of hardened sources. @@ -178,11 +178,12 @@ private static final class Wrapper extends SchemaFactory { * @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. */ - private static Source[] harden(final Source[] schemas) throws SAXException { + private Source[] harden(final Source[] schemas) throws SAXException { final Source[] hardened = new Source[schemas.length]; + final boolean overrideDefaultParser = overrideDefaultParser(); try { for (int i = 0; i < schemas.length; i++) { - hardened[i] = HardeningSAXParserFactory.harden(schemas[i]); + hardened[i] = HardeningSAXParserFactory.harden(schemas[i], overrideDefaultParser); } } catch (final TransformerConfigurationException e) { throw new SAXException("Failed to harden schema source", e); @@ -234,7 +235,7 @@ public boolean isSchemaLanguageSupported(final String schemaLanguage) { @Override public Schema newSchema() throws SAXException { - return new HardeningSchema(delegate.newSchema()); + return new HardeningSchema(delegate.newSchema(), overrideDefaultParser()); } /** @@ -245,7 +246,23 @@ public Schema newSchema() throws SAXException { */ @Override public Schema newSchema(final Source[] schemas) throws SAXException { - return new HardeningSchema(delegate.newSchema(harden(schemas))); + return new HardeningSchema(delegate.newSchema(harden(schemas)), overrideDefaultParser()); + } + + /** + * Checks whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}. + * + *

The JDK implementation of {@link SchemaFactory} uses the JDK parsers while {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or + * {@code false}.

+ * + * @return {@code true} if parsers should be created via {@code newInstance()}. + */ + private boolean overrideDefaultParser() { + try { + return delegate.getFeature(HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER); + } catch (final SAXNotRecognizedException | SAXNotSupportedException e) { + return true; + } } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningTemplates.java b/src/main/java/org/apache/commons/xml/HardeningTemplates.java index 56b339ee..8aa0fa71 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTemplates.java +++ b/src/main/java/org/apache/commons/xml/HardeningTemplates.java @@ -51,18 +51,26 @@ final class HardeningTemplates implements Templates { */ private final Supplier emptySource; + /** + * Snapshot of the factory's {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome, carried onto every produced Transformer and self-provisioned + * filter reader. + */ + final boolean overrideDefaultParser; + /** * Constructs a new instance. * - * @param delegate the delegate to wrap; must not be {@code null}. - * @param uriResolver the compile-time URIResolver snapshot to restore onto Transformers produced from the compiled Templates; may be {@code null}. - * @param emptySource the empty-{@link Source} supplier for the produced Transformers + * @param delegate the delegate to wrap; must not be {@code null}. + * @param uriResolver the compile-time URIResolver snapshot to restore onto Transformers produced from the compiled Templates; may be {@code null}. + * @param emptySource the empty-{@link Source} supplier for the produced Transformers. + * @param overrideDefaultParser whether the produced Transformers' source rewrites should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningTemplates(final Templates delegate, final URIResolver uriResolver, final Supplier emptySource) { + HardeningTemplates(final Templates delegate, final URIResolver uriResolver, final Supplier emptySource, final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); this.uriResolver = uriResolver; this.emptySource = emptySource; + this.overrideDefaultParser = overrideDefaultParser; } /** @@ -85,6 +93,6 @@ public Transformer newTransformer() throws TransformerConfigurationException { if (transformer == null) { return null; } - return new HardeningTransformer(transformer, uriResolver, emptySource); + return new HardeningTransformer(transformer, uriResolver, emptySource, overrideDefaultParser); } } diff --git a/src/main/java/org/apache/commons/xml/HardeningTemplatesHandler.java b/src/main/java/org/apache/commons/xml/HardeningTemplatesHandler.java index 814d9671..537d2add 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTemplatesHandler.java +++ b/src/main/java/org/apache/commons/xml/HardeningTemplatesHandler.java @@ -51,18 +51,26 @@ final class HardeningTemplatesHandler implements TemplatesHandler { */ private final Supplier emptySource; + /** + * Snapshot of the factory's {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome, carried onto the produced Templates. + */ + private final boolean overrideDefaultParser; + /** * Constructs a new instance. * * @param delegate the delegate to wrap; must not be {@code null}. * @param uriResolver the compile-time URIResolver snapshot to restore onto Transformers produced from the compiled Templates; may be {@code null}. * @param emptySource the empty-{@link Source} supplier for the produced Templates; may be {@code null} for the default empty DOM document. + * @param overrideDefaultParser whether the produced Templates' source rewrites should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningTemplatesHandler(final TemplatesHandler delegate, final URIResolver uriResolver, final Supplier emptySource) { + HardeningTemplatesHandler(final TemplatesHandler delegate, final URIResolver uriResolver, final Supplier emptySource, + final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); this.uriResolver = uriResolver; this.emptySource = emptySource; + this.overrideDefaultParser = overrideDefaultParser; } @Override @@ -94,7 +102,7 @@ public String getSystemId() { public Templates getTemplates() { // Null before the stylesheet's endDocument (and on a failed compile in some implementations). final Templates templates = delegate.getTemplates(); - return templates == null ? null : new HardeningTemplates(templates, uriResolver, emptySource); + return templates == null ? null : new HardeningTemplates(templates, uriResolver, emptySource, overrideDefaultParser); } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformer.java b/src/main/java/org/apache/commons/xml/HardeningTransformer.java index 39669d65..2229168e 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformer.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformer.java @@ -32,7 +32,7 @@ /** * {@link Transformer} wrapper that rewrites the Source on every {@link Transformer#transform(Source, Result)} call through - * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} calls a + * {@link HardeningSAXParserFactory#harden(Source, boolean)} before delegating, and keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} calls a * caller does not resolve return empty rather than being fetched. *

* The floor is installed on the delegate transformer at construction, seeded with the factory's compile-time resolver; {@link #setURIResolver(URIResolver)} @@ -51,18 +51,26 @@ final class HardeningTransformer extends Transformer { private final FallbackIgnoreURIResolver floor; + /** + * Snapshot of the factory's {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome at creation, like the JDK copies the feature onto the + * transformers it creates. + */ + private final boolean overrideDefaultParser; + /** * Constructs a new instance. * - * @param delegate the delegate to wrap; must not be {@code null}. - * @param uriResolver the compile-time URIResolver snapshot to seed the floor with; may be {@code null}. - * @param emptySource the empty-{@link Source} supplier for the produced Transformers; {@code null} for the default empty DOM document. + * @param delegate the delegate to wrap; must not be {@code null}. + * @param uriResolver the compile-time URIResolver snapshot to seed the floor with; may be {@code null}. + * @param emptySource the empty-{@link Source} supplier for the produced Transformers; {@code null} for the default empty DOM document. + * @param overrideDefaultParser whether the source rewrites should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningTransformer(final Transformer delegate, final URIResolver uriResolver, final Supplier emptySource) { + HardeningTransformer(final Transformer delegate, final URIResolver uriResolver, final Supplier emptySource, final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); this.uriResolver = uriResolver; - this.floor = new FallbackIgnoreURIResolver(uriResolver, emptySource); + this.overrideDefaultParser = overrideDefaultParser; + this.floor = new FallbackIgnoreURIResolver(uriResolver, emptySource, () -> overrideDefaultParser); delegate.setURIResolver(floor); } @@ -137,7 +145,7 @@ public void setURIResolver(final URIResolver resolver) { @Override public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException { try { - delegate.transform(HardeningSAXParserFactory.harden(xmlSource), outputTarget); + delegate.transform(HardeningSAXParserFactory.harden(xmlSource, overrideDefaultParser), outputTarget); } catch (final TransformerConfigurationException e) { throw new TransformerException(e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index b73c4d81..0908aa6e 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -185,7 +185,7 @@ private HardeningTransformerFactory() { } /** - * {@link TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link HardeningSAXParserFactory#harden(Source)} before + * {@link TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link HardeningSAXParserFactory#harden(Source, boolean)} before * delegating. * *

Used by providers whose underlying TrAX implementation pulls a new {@code SAXParserFactory.newInstance()} for any Source that is not already a @@ -220,21 +220,21 @@ private static final class Wrapper extends SAXTransformerFactory { /** * Parses a reader-less source into a DOM through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} and returns a {@link DOMSource} * carrying its system id, so the consumer walks the tree instead of provisioning its own reader. Any other source is left to - * {@link HardeningSAXParserFactory#harden(Source)}. + * {@link HardeningSAXParserFactory#harden(Source, boolean)}. * * @param source The source to scan for an associated stylesheet. - * @return A {@link DOMSource} for a reader-less source, otherwise the result of {@link HardeningSAXParserFactory#harden(Source)}. + * @return A {@link DOMSource} for a reader-less source, otherwise the result of {@link HardeningSAXParserFactory#harden(Source, boolean)}. * @throws TransformerConfigurationException if the source cannot be parsed. * @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. * @throws HardeningException Thrown if a (non-Andoid) factory cannot support the secure processing feature {@link XMLConstants#FEATURE_SECURE_PROCESSING}. */ - private static Source hardenSourceToDom(final Source source) throws TransformerConfigurationException { + private Source hardenSourceToDom(final Source source) throws TransformerConfigurationException { if (source instanceof StreamSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() == null) { final InputSource inputSource = SAXSource.sourceToInputSource(source); if (inputSource != null) { try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(overrideDefaultParser()); final Document document = factory.newDocumentBuilder().parse(inputSource); return new DOMSource(document, inputSource.getSystemId()); } catch (final ParserConfigurationException | SAXException | IOException e) { @@ -242,7 +242,7 @@ private static Source hardenSourceToDom(final Source source) throws TransformerC } } } - return HardeningSAXParserFactory.harden(source); + return HardeningSAXParserFactory.harden(source, overrideDefaultParser()); } /** @@ -255,6 +255,24 @@ private static boolean isXalan(final SAXTransformerFactory factory) { return factory.getClass().getName().startsWith("org.apache.xalan."); } + /** + * Whether the delegate recognizes {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER}, probed with a same-value {@code setFeature}: + * {@code TransformerFactory.getFeature} cannot signal an unrecognized name (it returns {@code false}), while every implementation rejects a + * {@code setFeature} for a name it does not support (Xalan with {@link TransformerConfigurationException}, Saxon with its own unchecked exception). + * + * @param factory The delegate factory. + * @return Whether the delegate recognizes the feature. + */ + private static boolean probeOverrideDefaultParser(final SAXTransformerFactory factory) { + try { + factory.setFeature(HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER, + factory.getFeature(HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER)); + return true; + } catch (final Exception e) { + return false; + } + } + private static Templates unwrap(final Templates templates) { return templates instanceof HardeningTemplates ? ((HardeningTemplates) templates).getDelegate() : templates; } @@ -268,6 +286,9 @@ private static Templates unwrap(final Templates templates) { private final FallbackIgnoreURIResolver floor; + /** Whether the delegate recognizes {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER}; its value is read per created product, like the JDK. */ + private final boolean supportsOverrideDefaultParser; + /** * Constructs a new instance. * @@ -289,7 +310,8 @@ private Wrapper(final SAXTransformerFactory delegate) { private Wrapper(final SAXTransformerFactory delegate, final Supplier emptySource) { this.delegate = Objects.requireNonNull(delegate, "delegate"); this.emptySource = emptySource; - this.floor = new FallbackIgnoreURIResolver(null, emptySource); + this.supportsOverrideDefaultParser = probeOverrideDefaultParser(delegate); + this.floor = new FallbackIgnoreURIResolver(null, emptySource, this::overrideDefaultParser); // Compile-time block for xsl:import/xsl:include and document(); a caller-set resolver is routed through the floor rather than replacing it. delegate.setURIResolver(floor); } @@ -304,7 +326,7 @@ private Wrapper(final SAXTransformerFactory delegate, final Supplier emp public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset) throws TransformerConfigurationException { // Xalan's getAssociatedStylesheet drops a SAXSource's reader and self-provisions its own to scan for xml-stylesheet PIs (XALANJ-2849). - final Source hardened = isXalan(delegate) ? hardenSourceToDom(source) : HardeningSAXParserFactory.harden(source); + final Source hardened = isXalan(delegate) ? hardenSourceToDom(source) : HardeningSAXParserFactory.harden(source, overrideDefaultParser()); return delegate.getAssociatedStylesheet(hardened, media, title, charset); } @@ -329,7 +351,7 @@ public URIResolver getURIResolver() { } private TransformerHandler hardenHandler(final TransformerHandler handler) { - return handler == null ? null : new HardeningTransformerHandler(handler, getURIResolver(), emptySource); + return handler == null ? null : new HardeningTransformerHandler(handler, getURIResolver(), emptySource, overrideDefaultParser()); } /** @@ -340,21 +362,21 @@ private TransformerHandler hardenHandler(final TransformerHandler handler) { */ @Override public Templates newTemplates(final Source source) throws TransformerConfigurationException { - final Templates templates = delegate.newTemplates(HardeningSAXParserFactory.harden(source)); - return templates == null ? null : new HardeningTemplates(templates, getURIResolver(), emptySource); + final Templates templates = delegate.newTemplates(HardeningSAXParserFactory.harden(source, overrideDefaultParser())); + return templates == null ? null : new HardeningTemplates(templates, getURIResolver(), emptySource, overrideDefaultParser()); } @Override public TemplatesHandler newTemplatesHandler() throws TransformerConfigurationException { final TemplatesHandler handler = delegate.newTemplatesHandler(); - return handler == null ? null : new HardeningTemplatesHandler(handler, getURIResolver(), emptySource); + return handler == null ? null : new HardeningTemplatesHandler(handler, getURIResolver(), emptySource, overrideDefaultParser()); } @Override public Transformer newTransformer() throws TransformerConfigurationException { // Identity transformer: still parses runtime sources, so wrap it to harden Transformer.transform(Source, Result). final Transformer transformer = delegate.newTransformer(); - return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver(), emptySource); + return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver(), emptySource, overrideDefaultParser()); } /** @@ -365,8 +387,8 @@ public Transformer newTransformer() throws TransformerConfigurationException { */ @Override public Transformer newTransformer(final Source source) throws TransformerConfigurationException { - final Transformer transformer = delegate.newTransformer(HardeningSAXParserFactory.harden(source)); - return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver(), emptySource); + final Transformer transformer = delegate.newTransformer(HardeningSAXParserFactory.harden(source, overrideDefaultParser())); + return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver(), emptySource, overrideDefaultParser()); } @Override @@ -382,7 +404,7 @@ public TransformerHandler newTransformerHandler() throws TransformerConfiguratio */ @Override public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException { - return hardenHandler(delegate.newTransformerHandler(HardeningSAXParserFactory.harden(source))); + return hardenHandler(delegate.newTransformerHandler(HardeningSAXParserFactory.harden(source, overrideDefaultParser()))); } @Override @@ -406,7 +428,7 @@ public XMLFilter newXMLFilter(final Source source) throws TransformerConfigurati @Override public XMLFilter newXMLFilter(final Templates templates) throws TransformerConfigurationException { return new HardeningXMLFilter(templates instanceof HardeningTemplates ? (HardeningTemplates) templates - : new HardeningTemplates(templates, getURIResolver(), emptySource)); + : new HardeningTemplates(templates, getURIResolver(), emptySource, overrideDefaultParser())); } @Override @@ -429,5 +451,17 @@ public void setFeature(final String name, final boolean value) throws Transforme public void setURIResolver(final URIResolver resolver) { floor.setDelegate(resolver); } + + /** + * Checks whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}. + * + *

The JDK implementation of {@link TransformerFactory} uses the JDK parsers while {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset + * or {@code false}.

+ * + * @return {@code true} if parsers should be created via {@code newInstance()}. + */ + private boolean overrideDefaultParser() { + return !supportsOverrideDefaultParser || delegate.getFeature(HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER); + } } } diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerHandler.java b/src/main/java/org/apache/commons/xml/HardeningTransformerHandler.java index 69000e98..0f467591 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerHandler.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerHandler.java @@ -54,11 +54,13 @@ final class HardeningTransformerHandler implements TransformerHandler { * @param delegate the delegate to wrap; must not be {@code null}. * @param uriResolver the compile-time URIResolver snapshot to restore onto the live transformer; may be {@code null}. * @param emptySource the empty-{@link Source} supplier for the produced Transformer's floor; {@code null} means the default empty DOM. + * @param overrideDefaultParser whether the live transformer's source rewrites should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningTransformerHandler(final TransformerHandler delegate, final URIResolver uriResolver, final Supplier emptySource) { + HardeningTransformerHandler(final TransformerHandler delegate, final URIResolver uriResolver, final Supplier emptySource, + final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); - this.transformer = new HardeningTransformer(delegate.getTransformer(), uriResolver, emptySource); + this.transformer = new HardeningTransformer(delegate.getTransformer(), uriResolver, emptySource, overrideDefaultParser); } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningValidator.java b/src/main/java/org/apache/commons/xml/HardeningValidator.java index 92d06fc8..d6a34164 100644 --- a/src/main/java/org/apache/commons/xml/HardeningValidator.java +++ b/src/main/java/org/apache/commons/xml/HardeningValidator.java @@ -34,7 +34,7 @@ /** * {@link Validator} wrapper that rewrites the Source on every {@link Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call through - * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and keeps an ignore-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is not resolved at + * {@link HardeningSAXParserFactory#harden(Source, boolean)} before delegating, and keeps an ignore-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is not resolved at * validation time. {@link #reset()} re-establishes the bare ignore-all floor, matching the just-constructed state. */ final class HardeningValidator extends Validator { @@ -43,14 +43,22 @@ final class HardeningValidator extends Validator { private final FallbackIgnoreLSResourceResolver floor = new FallbackIgnoreLSResourceResolver(null); + /** + * Snapshot of the factory's {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} outcome at creation, like the JDK copies the feature onto its + * validators. + */ + private 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 source rewrites should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningValidator(final Validator delegate) { + HardeningValidator(final Validator delegate, final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.overrideDefaultParser = overrideDefaultParser; // Block xsi:schemaLocation resolution; neither the JDK nor Xerces reliably propagates the factory's resolver to its Validators. The floor is a // non-removable lower bound: a caller opts specific lookups in by setting their own resolver, but cannot drop the ignore-all block. delegate.setResourceResolver(floor); @@ -113,7 +121,7 @@ public void setResourceResolver(final LSResourceResolver resourceResolver) { @Override public void validate(final Source source, final Result result) throws SAXException, IOException { try { - delegate.validate(HardeningSAXParserFactory.harden(source), result); + delegate.validate(HardeningSAXParserFactory.harden(source, overrideDefaultParser), result); } catch (final TransformerConfigurationException e) { throw new SAXException("Failed to harden source for validation", e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java index 1a8533e8..a72179b1 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java @@ -70,7 +70,7 @@ public void parse(final InputSource input) throws SAXException, IOException { } if (getParent() == null) { try { - setParent(HardeningSAXParserFactory.newHardenedReader()); + setParent(HardeningSAXParserFactory.newHardenedReader(templates.overrideDefaultParser)); } catch (final TransformerException e) { throw new SAXException(e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLReader.java b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java index 856aa673..9eae201d 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLReader.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLReader.java @@ -63,6 +63,15 @@ public ContentHandler getContentHandler() { return delegate.getContentHandler(); } + /** + * Gets the wrapped reader, so tests can observe which parser implementation a rewrite picked. + * + * @return The wrapped reader. + */ + XMLReader getDelegate() { + return delegate; + } + @Override public DTDHandler getDTDHandler() { return delegate.getDTDHandler(); diff --git a/src/main/java/org/apache/commons/xml/HardeningXPath.java b/src/main/java/org/apache/commons/xml/HardeningXPath.java index c843fea6..f36042d4 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPath.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPath.java @@ -55,7 +55,8 @@ final class HardeningXPath implements XPath { * Parses the source through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder}, mirroring the namespace awareness of the parser the * engine would have provisioned. * - * @param source The document to evaluate against. + * @param source The document to evaluate against. + * @param overrideDefaultParser Whether the document build should use the pluggable parser lookup instead of the platform's built-in parser. * @return The parsed document. * @throws NullPointerException if {@code source} is {@code null}, per the {@link XPath} contract. * @throws XPathExpressionException if the source cannot be parsed. @@ -63,10 +64,10 @@ final class HardeningXPath implements XPath { * configuration error} or if the implementation is not available or cannot be instantiated. * @throws HardeningException Thrown if a (non-Andoid) factory cannot support the secure processing feature {@link XMLConstants#FEATURE_SECURE_PROCESSING}. */ - static Document parse(final InputSource source) throws XPathExpressionException { + static Document parse(final InputSource source, final boolean overrideDefaultParser) throws XPathExpressionException { Objects.requireNonNull(source, "source"); try { - final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.newNSInstance(overrideDefaultParser); return factory.newDocumentBuilder().parse(source); } catch (final ParserConfigurationException | SAXException | IOException e) { throw new XPathExpressionException(e); @@ -75,20 +76,27 @@ static Document parse(final InputSource source) throws XPathExpressionException private final XPath delegate; + /** + * Snapshot of the factory's {@code jdk.xml.overrideDefaultParser} outcome at creation, like the JDK copies the feature onto the XPath objects it creates. + */ + 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 {@link InputSource} document builds should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningXPath(final XPath delegate) { + HardeningXPath(final XPath delegate, final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.overrideDefaultParser = overrideDefaultParser; } @Override public XPathExpression compile(final String expression) throws XPathExpressionException { final XPathExpression compiled = delegate.compile(expression); - return compiled == null ? null : new HardeningXPathExpression(compiled); + return compiled == null ? null : new HardeningXPathExpression(compiled, overrideDefaultParser); } /** @@ -99,7 +107,7 @@ public XPathExpression compile(final String expression) throws XPathExpressionEx */ @Override public String evaluate(final String expression, final InputSource source) throws XPathExpressionException { - return delegate.evaluate(expression, parse(source)); + return delegate.evaluate(expression, parse(source, overrideDefaultParser)); } /** @@ -110,7 +118,7 @@ public String evaluate(final String expression, final InputSource source) throws */ @Override public Object evaluate(final String expression, final InputSource source, final QName returnType) throws XPathExpressionException { - return delegate.evaluate(expression, parse(source), returnType); + return delegate.evaluate(expression, parse(source, overrideDefaultParser), returnType); } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathExpression.java b/src/main/java/org/apache/commons/xml/HardeningXPathExpression.java index 579bfa94..851dcb43 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathExpression.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathExpression.java @@ -38,14 +38,21 @@ final class HardeningXPathExpression implements XPathExpression { private final XPathExpression delegate; + /** + * Snapshot of the factory's {@code jdk.xml.overrideDefaultParser} outcome, inherited from the {@link HardeningXPath} that compiled this expression. + */ + private 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 {@link InputSource} document builds should use the pluggable parser lookup instead of the platform's built-in parser. * @throws NullPointerException if {@code delegate} is {@code null}. */ - HardeningXPathExpression(final XPathExpression delegate) { + HardeningXPathExpression(final XPathExpression delegate, final boolean overrideDefaultParser) { this.delegate = Objects.requireNonNull(delegate, "delegate"); + this.overrideDefaultParser = overrideDefaultParser; } /** @@ -56,7 +63,7 @@ final class HardeningXPathExpression implements XPathExpression { */ @Override public String evaluate(final InputSource source) throws XPathExpressionException { - return delegate.evaluate(HardeningXPath.parse(source)); + return delegate.evaluate(HardeningXPath.parse(source, overrideDefaultParser)); } /** @@ -67,7 +74,7 @@ public String evaluate(final InputSource source) throws XPathExpressionException */ @Override public Object evaluate(final InputSource source, final QName returnType) throws XPathExpressionException { - return delegate.evaluate(HardeningXPath.parse(source), returnType); + return delegate.evaluate(HardeningXPath.parse(source, overrideDefaultParser), returnType); } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index abfa65a3..0bf18ce8 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java @@ -65,8 +65,6 @@ public final class HardeningXPathFactory { * functions and reflection-based extension calls are reachable only through a locked-down Saxon {@code Configuration}, not the standard JAXP knobs; this * is the XPath counterpart of the Saxon exception in {@link HardeningTransformerFactory#harden(javax.xml.transform.TransformerFactory)}, kept as a * documented package-prefix exception because the required hardening surface is reachable only through a vendor API. - *
  • FODP ({@code jdk.xml.overrideDefaultParser}, set to {@code false}): best-effort. On the stock JDK it pins the internal parser lookup to - * the bundled SAX parser, blocking a system property swap to a third-party parser (defense-in-depth); Xalan rejects the feature and is left unchanged.
  • *
  • FSP ({@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only knob both the stock JDK and Xalan XPath * engines expose, and switches on their secure-processing limits. {@link XPathFactory} has no attribute API for finer control.
  • *
  • The nested wrapper: required. FSP governs only the engine, not the parser it provisions internally for the @@ -83,8 +81,6 @@ static XPathFactory harden(final XPathFactory factory) { // Saxon: only a locked-down Configuration can close its URI-fetching functions and extension-function surface. return SaxonProvider.configure(factory); } - // Best-effort: the stock JDK pins its bundled SAX parser (defense-in-depth); Xalan rejects the feature. - setOptionalFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); // Required: enables the engine's secure-processing limits; XPathFactory has no attribute API for finer control. setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); // Required: FSP does not reach the parser the engine provisions for InputSource-taking evaluate calls; the wrapper parses those itself. @@ -186,26 +182,6 @@ private static void setFeature(final XPathFactory factory, final String feature, } } - /** - * Sets an optional feature on the given factory, ignoring it if the implementation does not recognize it. - * - * @param factory The factory to harden. - * @param feature The feature to set. - * @param value The value to set. - */ - private static void setOptionalFeature(final XPathFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final XPathFactoryConfigurationException e) { - // Ignored: the implementation does not recognize this optional feature. - } - } - - /** - * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX parser; defense-in-depth against a system property swap to a third-party parser. - */ - private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; - private HardeningXPathFactory() { // static only } @@ -246,7 +222,7 @@ public boolean isObjectModelSupported(final String objectModel) { @Override public XPath newXPath() { final XPath xpath = delegate.newXPath(); - return xpath == null ? null : new HardeningXPath(xpath); + return xpath == null ? null : new HardeningXPath(xpath, overrideDefaultParser()); } @Override @@ -254,6 +230,22 @@ public void setFeature(final String name, final boolean value) throws XPathFacto delegate.setFeature(name, value); } + /** + * Checks whether parsers should be instantiated via {@code newInstance()} instead of {@code newDefaultInstance()}. + * + *

    The JDK implementation of {@link XPathFactory} uses the JDK parsers while {@value HardeningSAXParserFactory#OVERRIDE_DEFAULT_PARSER} is unset or + * {@code false}.

    + * + * @return {@code true} if parsers should be created via {@code newInstance()}. + */ + private boolean overrideDefaultParser() { + try { + return delegate.getFeature(HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER); + } catch (final XPathFactoryConfigurationException e) { + return true; + } + } + @Override public void setXPathFunctionResolver(final XPathFunctionResolver resolver) { delegate.setXPathFunctionResolver(resolver); diff --git a/src/main/java/org/apache/commons/xml/SaxonProvider.java b/src/main/java/org/apache/commons/xml/SaxonProvider.java index 935b422d..13a2c221 100644 --- a/src/main/java/org/apache/commons/xml/SaxonProvider.java +++ b/src/main/java/org/apache/commons/xml/SaxonProvider.java @@ -111,7 +111,7 @@ private static TransformerFactory configure(final TransformerFactory factory) { private static XPathFactory configure(final XPathFactory factory) { final HardenedConfiguration config = new HardenedConfiguration(); // XPath has no factory wrapper, so the ignore-all floor lives on the Configuration; reuse FallbackIgnoreURIResolver, adapted to a ResourceResolver. - config.setResourceResolver(new ResourceResolverWrappingURIResolver(new FallbackIgnoreURIResolver(null, emptySourceSupplier()))); + config.setResourceResolver(new ResourceResolverWrappingURIResolver(new FallbackIgnoreURIResolver(null, emptySourceSupplier(), () -> false))); ((XPathFactoryImpl) factory).setConfiguration(config); return factory; } 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 c837c8c1..aaeaf318 100644 --- a/src/main/java/org/apache/commons/xml/package-info.java +++ b/src/main/java/org/apache/commons/xml/package-info.java @@ -21,6 +21,23 @@ * Every method returns new, hardened factory instances. No caching or pooling is performed; callers on a hot path are responsible for their own * caching. *

    + *

    + * A returned factory is not necessarily an instance of the underlying implementation. It might be (and usually is) a wrapper around it, so it cannot be cast + * to the implementation's own class. Everything else about the implementation's behavior is preserved: features, properties, and attributes delegate to it, + * and only the security behavior is hardened. + *

    + *

    + * Preserved behavior includes the choice of internal parsers. Each TrAX, XPath, or schema implementation has its own way of instantiating them, and the + * library respects it: + *

    + *
      + *
    • Stock JDK factories use the JDK parsers by default, and expose the {@code jdk.xml.overrideDefaultParser} feature (and Java system property of the same + * name) to switch to parsers instantiated through {@link java.util.ServiceLoader}.
    • + *
    • Saxon selects its parsers through its own configuration.
    • + *
    + *

    + * Whichever parser is selected, it is hardened. + *

    *

    Hardening guarantees

    *

    * Every factory returned by makes the same three guarantees, regardless of which JAXP implementation is on the classpath: diff --git a/src/site/markdown/index.md b/src/site/markdown/index.md index f8801f0a..f3f4960a 100644 --- a/src/site/markdown/index.md +++ b/src/site/markdown/index.md @@ -146,6 +146,27 @@ HardeningSchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI) .validate(new StreamSource(inputStream)); ``` +### Wrappers, not the original factories + +A returned factory is not necessarily an instance of the underlying implementation. +It might be (and usually is) a wrapper around it, +so it cannot be cast to the implementation's own class. +Everything else about the implementation's behavior is preserved: +features, properties, and attributes delegate to it, +and only the security behavior is hardened. + +Preserved behavior includes the choice of internal parsers. +Each TrAX, XPath, or schema implementation has its own way of instantiating them, +and the library respects it: + +- Stock JDK factories use the JDK parsers by default, + and expose the `jdk.xml.overrideDefaultParser` feature + (and Java system property of the same name) + to switch to parsers instantiated through `ServiceLoader`. +- Saxon selects its parsers through its own configuration. + +Whichever parser is selected, it is hardened. + ### Factory methods Each factory class mirrors every static factory method its JAXP counterpart offers, diff --git a/src/site/markdown/threat_model.md b/src/site/markdown/threat_model.md index 1532cbf1..79056886 100644 --- a/src/site/markdown/threat_model.md +++ b/src/site/markdown/threat_model.md @@ -161,7 +161,6 @@ produces, breaks the hardening for that instance. - `http://xml.org/sax/features/external-parameter-entities` - `javax.xml.stream.isSupportingExternalEntities` - `javax.xml.stream.supportDTD` -- `jdk.xml.overrideDefaultParser` - the implementation's secure-processing limits (entity expansion, element depth, attribute count, and similar) This list is not exhaustive: @@ -219,6 +218,13 @@ enforced by the reserved settings above, which a caller cannot lift. As in the previous case, you need to provide a secure resolver. +- **Internal parser selection.** + On the stock JDK TrAX, XPath, and schema implementations + you may set [`jdk.xml.overrideDefaultParser`](https://docs.oracle.com/en/java/javase/25/docs/api/java.xml/module-summary.html#jdk.xml.overrideDefaultParser) + to switch their internal parses from the JDK parsers to a `ServiceLoader`-resolved parser. + Whichever parser is selected, it is hardened, + so the setting carries no security weight. + ### What is out of scope A returned factory is hardened as delivered; reconfiguring it is a decision to take over hardening for that instance, diff --git a/src/test/java/org/apache/commons/xml/DenyUnresolvedTest.java b/src/test/java/org/apache/commons/xml/DenyUnresolvedTest.java index 54b25b7f..62bee1d9 100644 --- a/src/test/java/org/apache/commons/xml/DenyUnresolvedTest.java +++ b/src/test/java/org/apache/commons/xml/DenyUnresolvedTest.java @@ -56,7 +56,7 @@ void floorsThrowOnUnresolved() { "XMLResolver floor should throw on an unresolved entity"); assertThrows(LSException.class, () -> new FallbackIgnoreLSResourceResolver(null).resolveResource(null, null, null, SYSTEM_ID, null), "LSResourceResolver floor should throw on an unresolved resource"); - assertThrows(TransformerException.class, () -> new FallbackIgnoreURIResolver(null, null).resolve(SYSTEM_ID, null), + assertThrows(TransformerException.class, () -> new FallbackIgnoreURIResolver(null, null, () -> false).resolve(SYSTEM_ID, null), "URIResolver floor should throw on an unresolved URI"); } } diff --git a/src/test/java/org/apache/commons/xml/OverrideDefaultParserTest.java b/src/test/java/org/apache/commons/xml/OverrideDefaultParserTest.java new file mode 100644 index 00000000..ef67fef2 --- /dev/null +++ b/src/test/java/org/apache/commons/xml/OverrideDefaultParserTest.java @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeFalse; + +import java.io.StringWriter; + +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.stream.StreamResult; +import javax.xml.validation.SchemaFactory; +import javax.xml.xpath.XPathFactory; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledInNativeImage; +import org.xml.sax.XMLReader; + +/** + * Checks that {@code jdk.xml.overrideDefaultParser} selects which hardened parser family performs the source rewrites on factories that recognize the feature. + * + *

    The wrapped implementations' internal parsers are never used — the wrappers parse every source themselves — so instead of configuring the delegate the + * wrappers read the feature: {@code false} (the JDK's default) pins the platform's built-in parser, {@code true} (or a delegate that does not recognize the + * feature) keeps the pluggable lookup. Both choices are hardened, so the feature carries no security weight. The tests pin the JDK implementations through + * {@code newDefaultInstance()}, so they discriminate in every JVM execution; under test-jdk-xerces the two parser families genuinely differ.

    + */ +@Tag("trax") +@Tag("xpath") +@Tag("schema") +class OverrideDefaultParserTest { + + private static final String FEATURE = HardeningSAXParserFactory.OVERRIDE_DEFAULT_PARSER; + + /** Package prefix of the JDK's built-in parsers, the family a {@code false} feature value pins. */ + private static final String JDK_INTERNAL_PREFIX = "com.sun.org.apache.xerces.internal."; + + private static String transform(final TransformerFactory factory, final String text) throws Exception { + final Transformer transformer = factory.newTransformer(AttackTestSupport.streamSource(AttackTestSupport.xsltBody(text))); + final StringWriter out = new StringWriter(); + transformer.transform(AttackTestSupport.streamSource(AttackTestSupport.xmlBody("ignored")), new StreamResult(out)); + return out.toString(); + } + + @Test + void hardenedReaderFollowsFlag() throws Exception { + assumeFalse(AttackTestSupport.IS_ANDROID); + final XMLReader pinned = ((HardeningXMLReader) HardeningSAXParserFactory.newHardenedReader(false)).getDelegate(); + assertTrue(pinned.getClass().getName().startsWith(JDK_INTERNAL_PREFIX), pinned.getClass().getName()); + final XMLReader pluggable = ((HardeningXMLReader) HardeningSAXParserFactory.newHardenedReader(true)).getDelegate(); + final XMLReader lookedUp = ((HardeningXMLReader) HardeningSAXParserFactory.newNSInstance().newSAXParser().getXMLReader()).getDelegate(); + assertEquals(lookedUp.getClass(), pluggable.getClass()); + if (xercesOnClasspath()) { + // The two families genuinely differ only where a third-party parser wins the lookup (the test-jdk-xerces execution). + assertNotEquals(pinned.getClass(), pluggable.getClass()); + } + } + + @Test + void schemaFactoryReadsFeatureAtCreation() throws Exception { + assumeFalse(AttackTestSupport.IS_ANDROID); + final SchemaFactory factory = HardeningSchemaFactory.newDefaultInstance(); + assertFalse(factory.getFeature(FEATURE)); + assertFalse(((HardeningSchema) factory.newSchema(AttackTestSupport.streamSource(AttackTestSupport.BENIGN_SCHEMA))).overrideDefaultParser); + factory.setFeature(FEATURE, true); + assertTrue(((HardeningSchema) factory.newSchema(AttackTestSupport.streamSource(AttackTestSupport.BENIGN_SCHEMA))).overrideDefaultParser); + } + + @Test + void transformerFactoryReadsFeatureAtCreation() throws Exception { + assumeFalse(AttackTestSupport.IS_ANDROID); + final TransformerFactory factory = HardeningTransformerFactory.newDefaultInstance(); + assertFalse(factory.getFeature(FEATURE)); + assertFalse(((HardeningTemplates) factory.newTemplates(AttackTestSupport.streamSource(AttackTestSupport.xsltBody("probe")))).overrideDefaultParser); + factory.setFeature(FEATURE, true); + assertTrue(((HardeningTemplates) factory.newTemplates(AttackTestSupport.streamSource(AttackTestSupport.xsltBody("probe")))).overrideDefaultParser); + } + + @Test + // The JDK default TrAX pinned by newDefaultInstance() is XSLTC, which defines the compiled translet class at run time — impossible in a closed-world + // native image (the reason the native profile substitutes Xalan). The capture tests above stay enabled: newTemplates never loads the translet. + @DisabledInNativeImage + void transformSucceedsUnderBothParserFamilies() throws Exception { + assumeFalse(AttackTestSupport.IS_ANDROID); + final TransformerFactory factory = HardeningTransformerFactory.newDefaultInstance(); + // Feature false (the JDK's default): stylesheet and source parse through the pinned platform parser. + assertTrue(transform(factory, "pinned").contains("pinned")); + factory.setFeature(FEATURE, true); + // Feature true: same result through the pluggable lookup. + assertTrue(transform(factory, "pluggable").contains("pluggable")); + } + + private static boolean xercesOnClasspath() { + try { + Class.forName("org.apache.xerces.jaxp.SAXParserFactoryImpl"); + return true; + } catch (final ClassNotFoundException e) { + return false; + } + } + + @Test + void xPathFactoryReadsFeatureAtCreation() throws Exception { + assumeFalse(AttackTestSupport.IS_ANDROID); + final XPathFactory factory = HardeningXPathFactory.newDefaultInstance(); + assertFalse(factory.getFeature(FEATURE)); + assertFalse(((HardeningXPath) factory.newXPath()).overrideDefaultParser); + factory.setFeature(FEATURE, true); + assertTrue(((HardeningXPath) factory.newXPath()).overrideDefaultParser); + } +} diff --git a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java index cdd9c3d4..bfe669b6 100644 --- a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java +++ b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java @@ -90,7 +90,7 @@ class ShadingFootprintTest { // @formatter:on /** - * TrAX, XPath and schema re-harden their sub-parsers through {@link HardeningSAXParserFactory#harden(Source)}, so each builds on the full SAX closure below; + * TrAX, XPath and schema re-harden their sub-parsers through {@link HardeningSAXParserFactory#harden(Source, boolean)}, so each builds on the full SAX closure below; * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM entry point, so * their closures carry that set too. */ diff --git a/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java b/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java index 18231b83..198ad484 100644 --- a/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java +++ b/src/test/java/org/apache/commons/xml/XPathInputSourceTest.java @@ -33,7 +33,7 @@ *

    The stock JDK and Apache Xalan implement the {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points by provisioning an internal document * parser that {@code FEATURE_SECURE_PROCESSING} on the {@link XPathFactory} does not reach. The {@link HardeningXPathFactory} wrapper parses the input * through a hardened {@code DocumentBuilder} instead, so the external reference resolves to empty on the floor, while the - * evaluation itself still works. Tagged {@code xpath}, so it runs under test-stockjdk, test-xalan and test-xalan-xerces; the Saxon engine takes the separate + * evaluation itself still works. Tagged {@code xpath}, so it runs under test-stockjdk, test-jdk-xerces, test-xalan and test-xalan-xerces; the Saxon engine takes the separate * {@code SaxonProvider} path covered by {@code SaxonXPathExternalCallsTest}.

    */ @Tag("xpath")