From b7c9414c52a35e40ef4f22dfb422643a6a3a6526 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Sun, 5 Jul 2026 22:22:32 +0200 Subject: [PATCH 1/3] Base TransformerFactory hardening on feature support Replace the class-name dispatch(TransformerFactory) switch with a capability-driven TransformerHardener, mirroring the DOM and SAX hardeners. It probes what the factory supports rather than branching per vendor, keeping a single documented class-name exception for Saxon, whose hardening (a Configuration that closes every resource-resolution channel plus the extension-function surface) is reachable only through a vendor API. Stock JDK XSLTC and Apache Xalan now share one recipe: FSP, best-effort limits and ACCESS_EXTERNAL_* (defense-in-depth where honoured), a deny-all URIResolver as the required block for xsl:import/xsl:include and document(), and a HardeningTransformerFactory wrapper. XSLTC previously relied on ACCESS_EXTERNAL_STYLESHEET as its required block and Xalan on the resolver; both now use the resolver, so ACCESS_EXTERNAL_* drops to best-effort. Both block paths surface as TransformerException, already accepted by the tests. Supporting changes: JaxpSetters gains setOptionalAttribute(TransformerFactory) and loses the now-unused strict setAttribute overload; Limits.applyToJdkTransformer becomes best-effort tryApply(TransformerFactory); StockJdkProvider and XalanProvider drop their configure(TransformerFactory) recipes (SaxonProvider is unchanged, still shared with XPath). Assisted-By: Claude Opus 4.8 --- .../org/apache/commons/xml/JaxpSetters.java | 12 ++- .../java/org/apache/commons/xml/Limits.java | 22 +++-- .../apache/commons/xml/StockJdkProvider.java | 16 --- .../commons/xml/TransformerHardener.java | 99 +++++++++++++++++++ .../org/apache/commons/xml/XalanProvider.java | 34 +------ .../org/apache/commons/xml/XmlFactories.java | 21 +--- 6 files changed, 125 insertions(+), 79 deletions(-) create mode 100644 src/main/java/org/apache/commons/xml/TransformerHardener.java diff --git a/src/main/java/org/apache/commons/xml/JaxpSetters.java b/src/main/java/org/apache/commons/xml/JaxpSetters.java index b73a6de..486476a 100644 --- a/src/main/java/org/apache/commons/xml/JaxpSetters.java +++ b/src/main/java/org/apache/commons/xml/JaxpSetters.java @@ -57,10 +57,6 @@ static void setAttribute(final DocumentBuilderFactory factory, final String attr apply(factory, KIND_ATTRIBUTE, attribute, () -> factory.setAttribute(attribute, value)); } - static void setAttribute(final TransformerFactory factory, final String attribute, final Object value) { - apply(factory, KIND_ATTRIBUTE, attribute, () -> factory.setAttribute(attribute, value)); - } - static void setFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) { apply(factory, KIND_FEATURE, feature, () -> factory.setFeature(feature, value)); } @@ -97,6 +93,14 @@ static void setOptionalAttribute(final DocumentBuilderFactory factory, final Str trySetAttribute(factory, attribute, value); } + static void setOptionalAttribute(final TransformerFactory factory, final String attribute, final Object value) { + try { + factory.setAttribute(attribute, value); + } catch (final Exception e) { + // Ignored: the implementation does not recognize this attribute. + } + } + static void setOptionalFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) { try { factory.setFeature(feature, value); diff --git a/src/main/java/org/apache/commons/xml/Limits.java b/src/main/java/org/apache/commons/xml/Limits.java index 8145a0f..7b4e0cd 100644 --- a/src/main/java/org/apache/commons/xml/Limits.java +++ b/src/main/java/org/apache/commons/xml/Limits.java @@ -244,15 +244,6 @@ final class Limits { JDK_LIMITS = Collections.unmodifiableMap(map); } - /** - * Sets every JDK-supported limit on a stock JDK {@link TransformerFactory}. - * - * @param factory The target factory to modify. - */ - static void applyToJdkTransformer(final TransformerFactory factory) { - JDK_LIMITS.forEach((name, supplier) -> setAttribute(factory, name, Integer.toString(supplier.getAsInt()))); - } - /** * Sets every JDK-supported limit on a Xerces {@code org.apache.xerces.util.SecurityManager}. * @@ -347,6 +338,19 @@ static void tryApply(final DocumentBuilderFactory factory) { JDK_LIMITS.forEach((name, supplier) -> setOptionalAttribute(factory, name, Integer.toString(supplier.getAsInt()))); } + /** + * Best-effort application of the JDK processing limits to a {@link TransformerFactory}. + * + *

The stock JDK's XSLTC honours the JDK limit attributes, so this pins them to JDK 25 secure values; other implementations (Apache Xalan) reject the + * attributes and their caps come from {@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING} instead. Neither path throws if the implementation declines a + * limit.

+ * + * @param factory The target factory to modify. + */ + static void tryApply(final TransformerFactory factory) { + JDK_LIMITS.forEach((name, supplier) -> setOptionalAttribute(factory, name, Integer.toString(supplier.getAsInt()))); + } + /** * Best-effort application of the processing limits to an {@link XMLInputFactory}, regardless of implementation. * diff --git a/src/main/java/org/apache/commons/xml/StockJdkProvider.java b/src/main/java/org/apache/commons/xml/StockJdkProvider.java index efb8839..7e5b676 100644 --- a/src/main/java/org/apache/commons/xml/StockJdkProvider.java +++ b/src/main/java/org/apache/commons/xml/StockJdkProvider.java @@ -17,13 +17,10 @@ package org.apache.commons.xml; -import static org.apache.commons.xml.JaxpSetters.setAttribute; import static org.apache.commons.xml.JaxpSetters.setFeature; import javax.xml.XMLConstants; import javax.xml.parsers.SAXParserFactory; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.xpath.XPathFactory; import org.xml.sax.XMLReader; @@ -52,19 +49,6 @@ final class StockJdkProvider { */ private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; - static TransformerFactory configure(final TransformerFactory factory) { - // Required: enables XSLTC's runtime evaluator limits (entity expansion, attribute count, element/name depth). - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - // Defense-in-depth: pin to JDK 25 limits so older JDKs do not fall back to looser secure values. - Limits.applyToJdkTransformer(factory); - // Required: XSLTC's compile path (Util.getInputSource) propagates the factory's ACCESS_EXTERNAL_DTD onto the SAXSource's reader. - setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); - // Required: Prevents resolution of `xsl:import`, `xsl:include` and `document()`. - setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, ""); - // Required: XSLTC's source-document parsing path provisions its own SAX reader if the source does not have its own parser. - return new HardeningTransformerFactory((SAXTransformerFactory) factory); - } - static XPathFactory configure(final XPathFactory factory) { // Defense-in-depth: pin to the JDK's bundled SAX parser; see FEATURE_OVERRIDE_DEFAULT_PARSER. setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java b/src/main/java/org/apache/commons/xml/TransformerHardener.java new file mode 100644 index 0000000..409e89c --- /dev/null +++ b/src/main/java/org/apache/commons/xml/TransformerHardener.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import static org.apache.commons.xml.JaxpSetters.setFeature; +import static org.apache.commons.xml.JaxpSetters.setOptionalAttribute; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.XMLConstants; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.URIResolver; +import javax.xml.transform.sax.SAXTransformerFactory; + +/** + * Capability-driven hardening for any {@link TransformerFactory} on the classpath. + * + *

Rather than branching on the implementation class, {@link #harden(TransformerFactory)} probes what the factory supports and adapts:

+ * + * + *

Caveats

+ * + */ +final class TransformerHardener { + + /** + * Class names of Saxon's {@link TransformerFactory} (open-source and commercial editions), hardened through a Saxon {@code Configuration} rather than the + * standard JAXP knobs. + */ + private static final Set SAXON_TRANSFORMER_FACTORIES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "net.sf.saxon.TransformerFactoryImpl", + "com.saxonica.config.ProfessionalTransformerFactory", + "com.saxonica.config.EnterpriseTransformerFactory"))); + + static TransformerFactory harden(final TransformerFactory factory) { + if (SAXON_TRANSFORMER_FACTORIES.contains(factory.getClass().getName())) { + // Saxon: only a locked-down Configuration can close all of its resource-resolution channels and its extension-function surface. + return SaxonProvider.configure(factory); + } + // Required: enables secure processing (XSLTC runtime limits; Xalan's extension-function block). + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Best-effort: JDK's XSLTC honors the JDK attribute limits, pinning them to JDK 25 secure values; Xalan ignores them. + Limits.tryApply(factory); + // Required on JDK's XSLTC: it copies this factory attribute onto the reader that parses the stylesheet (Util.getInputSource). + // A permissive default here would re-open the external-DTD/entity channel. + // Xalan rejects the attribute and blocks that channel through a deny-all resolver instead. + setOptionalAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, ""); + // Required: the one channel both implementations honor; blocks xsl:import/include at compile time and document() at runtime. + factory.setURIResolver(Resolvers.DenyAll.URI); + // Required: source/stylesheet parsing provisions its own SAX reader otherwise; the wrapper routes every Source through a hardened one. + return new HardeningTransformerFactory((SAXTransformerFactory) factory); + } + + private TransformerHardener() { + } +} diff --git a/src/main/java/org/apache/commons/xml/XalanProvider.java b/src/main/java/org/apache/commons/xml/XalanProvider.java index 21a3f7c..86980de 100644 --- a/src/main/java/org/apache/commons/xml/XalanProvider.java +++ b/src/main/java/org/apache/commons/xml/XalanProvider.java @@ -20,8 +20,6 @@ import static org.apache.commons.xml.JaxpSetters.setFeature; import javax.xml.XMLConstants; -import javax.xml.transform.TransformerFactory; -import javax.xml.transform.sax.SAXTransformerFactory; import javax.xml.xpath.XPathFactory; /** @@ -30,38 +28,12 @@ *

Factory classes live under the {@code org.apache.xalan.*} and {@code org.apache.xpath.*} packages. Xalan ships only TrAX and XPath; its DOM, SAX, StAX and * Schema needs are served by whatever JDK or external Xerces is on the classpath.

* - *

Hardening recipe applied to every factory below uses the same building blocks:

- *
    - *
  • FSP ({@link XMLConstants#FEATURE_SECURE_PROCESSING}, set to {@code true}): enables Xalan's secure-processing mode, which disables - * reflection-based extension functions. Required.
  • - *
  • {@link Resolvers.DenyAll#URI}: required. Xalan does not implement the JAXP 1.5 {@code ACCESS_EXTERNAL_*} attributes; a deny-all - * {@link javax.xml.transform.URIResolver} blocks {@code xsl:include}, {@code xsl:import}, {@code xsl:source-document} during stylesheet compilation - * and {@code document()}, {@code unparsed-text()}, {@code collection()} at runtime.
  • - *
  • {@link HardeningTransformerFactory} + {@link HardeningTemplates} + {@link HardeningTransformer}: required. Xalan's source-document - * parsing path falls back to {@code SAXParserFactory.newInstance()} whenever the input is not a {@link javax.xml.transform.dom.DOMSource} or a - * {@link javax.xml.transform.sax.SAXSource} carrying its own {@link org.xml.sax.XMLReader}; it only sets FSP on the resulting reader. The wrappers - * rewrite every Source through an {@link XmlFactories}-hardened reader so the Xalan internals never get to provision their own.
  • - *
- * - *

Caveats

- *
    - *
  • Replacing the {@code URIResolver} on the factory or transformer cancels the deny-all hardening for XSLT URI fetches; Xalan exposes no - * tamper-resistant equivalent of JAXP 1.5 {@code ACCESS_EXTERNAL_STYLESHEET}.
  • - *
+ *

This class only handles XPath. TrAX hardening is capability-driven across all implementations and lives in {@link TransformerFactoryHardener}, which applies + * to Xalan the same building blocks it applies to XSLTC (FSP, a deny-all {@link javax.xml.transform.URIResolver} for {@code xsl:import}/{@code xsl:include} and + * {@code document()}, and a {@link HardeningTransformerFactory} wrapper so source parsing runs through an {@link XmlFactories}-hardened reader).

*/ final class XalanProvider { - static TransformerFactory configure(final TransformerFactory factory) { - // Required: enables Xalan's secure-processing mode (extension-function block) - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - // Required: Xalan does not honour JAXP 1.5 ACCESS_EXTERNAL_*; the deny-all URIResolver blocks: - // xsl:import/xsl:include at compile time and document()/unparsed-text() at runtime. - factory.setURIResolver(Resolvers.DenyAll.URI); - // Required: Xalan's internal SAX reader is sourced from SAXParserFactory.newInstance() and only carries FSP. - // We replace it with our hardened factory - return new HardeningTransformerFactory((SAXTransformerFactory) factory); - } - static XPathFactory configure(final XPathFactory factory) { // Required: enables Xalan's secure-processing mode; XPathFactory has no property API for finer control. setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); diff --git a/src/main/java/org/apache/commons/xml/XmlFactories.java b/src/main/java/org/apache/commons/xml/XmlFactories.java index ca1a9c6..1815457 100644 --- a/src/main/java/org/apache/commons/xml/XmlFactories.java +++ b/src/main/java/org/apache/commons/xml/XmlFactories.java @@ -73,22 +73,6 @@ */ public final class XmlFactories { - private static TransformerFactory dispatch(final TransformerFactory factory) { - switch (factory.getClass().getName()) { - case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl": - return StockJdkProvider.configure(factory); - case "org.apache.xalan.processor.TransformerFactoryImpl": - case "org.apache.xalan.xsltc.trax.TransformerFactoryImpl": - return XalanProvider.configure(factory); - case "net.sf.saxon.TransformerFactoryImpl": - case "com.saxonica.config.ProfessionalTransformerFactory": - case "com.saxonica.config.EnterpriseTransformerFactory": - return SaxonProvider.configure(factory); - default: - throw noProvider(factory); - } - } - private static XPathFactory dispatch(final XPathFactory factory) { switch (factory.getClass().getName()) { case "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl": @@ -196,11 +180,10 @@ public static SchemaFactory newSchemaFactory() { * {@code Transformer.transform(Source, Result)} time.

* * @return a hardened factory. - * @throws IllegalStateException if the underlying TrAX implementation is not recognized by any bundled hardening recipe, or if the matching recipe cannot - * apply its settings to it. + * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. */ public static TransformerFactory newTransformerFactory() { - return dispatch(TransformerFactory.newInstance()); + return TransformerHardener.harden(TransformerFactory.newInstance()); } /** From a0b6ed89adf92e7d309ab6efa8cec2d77f4a5a37 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 6 Jul 2026 12:07:06 +0200 Subject: [PATCH 2/3] Base XPathFactory hardening on feature support Replace the class-name dispatch(XPathFactory) switch with a capability-driven XPathHardener, mirroring TransformerHardener. The XPath object model splits the same way as TrAX: the stock JDK and Apache Xalan ship an XPath 1.0 engine with no URI-fetching functions, while Saxon adds the XPath 3.1 fn:doc, fn:collection and fn:unparsed-text functions. So it keeps one documented class-name exception for Saxon (net.sf.saxon plus the com.saxonica.config Professional/Enterprise editions, which subclass the open-source XPathFactoryImpl), handed to SaxonProvider for a locked-down Configuration; every other engine gets FSP (required) and best-effort FODP (stock JDK pins its bundled parser, Xalan rejects it). The two capability-driven hardeners are named after the product rather than the factory, matching DocumentBuilderHardener and SAXParserHardener. Supporting changes: JaxpSetters gains setOptionalFeature(XPathFactory); StockJdkProvider and XalanProvider are removed, since configure(XPathFactory) was their last remaining recipe (SaxonProvider is unchanged, shared with TrAX). Assisted-By: Claude Opus 4.8 --- .../org/apache/commons/xml/JaxpSetters.java | 8 ++ .../apache/commons/xml/StockJdkProvider.java | 62 --------------- .../org/apache/commons/xml/XPathHardener.java | 78 +++++++++++++++++++ .../org/apache/commons/xml/XalanProvider.java | 45 ----------- .../org/apache/commons/xml/XmlFactories.java | 22 +----- 5 files changed, 88 insertions(+), 127 deletions(-) delete mode 100644 src/main/java/org/apache/commons/xml/StockJdkProvider.java create mode 100644 src/main/java/org/apache/commons/xml/XPathHardener.java delete mode 100644 src/main/java/org/apache/commons/xml/XalanProvider.java diff --git a/src/main/java/org/apache/commons/xml/JaxpSetters.java b/src/main/java/org/apache/commons/xml/JaxpSetters.java index 486476a..6e056dd 100644 --- a/src/main/java/org/apache/commons/xml/JaxpSetters.java +++ b/src/main/java/org/apache/commons/xml/JaxpSetters.java @@ -117,6 +117,14 @@ static void setOptionalFeature(final XMLReader reader, final String feature, fin } } + static void setOptionalFeature(final XPathFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final Exception e) { + // Ignored: the implementation does not recognize this feature. + } + } + static void setOptionalProperty(final XMLInputFactory factory, final String property, final Object value) { trySetProperty(factory, property, value); } diff --git a/src/main/java/org/apache/commons/xml/StockJdkProvider.java b/src/main/java/org/apache/commons/xml/StockJdkProvider.java deleted file mode 100644 index 7e5b676..0000000 --- a/src/main/java/org/apache/commons/xml/StockJdkProvider.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.xml; - -import static org.apache.commons.xml.JaxpSetters.setFeature; - -import javax.xml.XMLConstants; -import javax.xml.parsers.SAXParserFactory; -import javax.xml.xpath.XPathFactory; - -import org.xml.sax.XMLReader; - -/** - * Hardening recipes for the stock JDK's JAXP implementation. - * - *

This is the internal fork of Apache Xerces, Xalan and friends shipped inside {@code com.sun.org.apache.*} and {@code com.sun.xml.internal.*} packages.

- * - *

Hardening recipe applied to every factory below uses the same building blocks:

- *
    - *
  • FODP ({@link #FEATURE_OVERRIDE_DEFAULT_PARSER}, set to {@code false}): pins the internal {@link XMLReader} lookup to the JDK's - * bundled SAX parser instead of {@link SAXParserFactory#newInstance()}, blocking a sysprop swap to a third-party parser. Defense-in-depth.
  • - *
  • FSP ({@link XMLConstants#FEATURE_SECURE_PROCESSING}, set to {@code true}): switches the JDK's {@code XMLSecurityManager} into secure - * mode, which is what enables the JDK-side processing limits in the first place. Required.
  • - *
  • {@code Limits.applyToJdk*}: defense-in-depth, pinning the limits to JDK 25 secure values so older JDKs do not fall back to looser - * defaults.
  • - *
  • {@code ACCESS_EXTERNAL_*}: already the FSP-secure default but set to {@code ""} explicitly so a sysprop ({@code - * javax.xml.accessExternal*}) cannot loosen them.
  • - *
- */ -final class StockJdkProvider { - - /** - * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX parser; defense-in-depth against a sysprop swap to a third-party parser. - */ - private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; - - static XPathFactory configure(final XPathFactory factory) { - // Defense-in-depth: pin to the JDK's bundled SAX parser; see FEATURE_OVERRIDE_DEFAULT_PARSER. - setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); - // Required: enables JDK XPath limits; XPathFactory has no property API for finer control. - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - return factory; - } - - private StockJdkProvider() { - } -} diff --git a/src/main/java/org/apache/commons/xml/XPathHardener.java b/src/main/java/org/apache/commons/xml/XPathHardener.java new file mode 100644 index 0000000..488c641 --- /dev/null +++ b/src/main/java/org/apache/commons/xml/XPathHardener.java @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.xml; + +import static org.apache.commons.xml.JaxpSetters.setFeature; +import static org.apache.commons.xml.JaxpSetters.setOptionalFeature; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.xml.XMLConstants; +import javax.xml.xpath.XPathFactory; + +/** + * Capability-driven hardening for any {@link XPathFactory} on the classpath. + * + *

The XPath object model mirrors TrAX: the stock JDK and Apache Xalan ship an XPath 1.0 engine with no URI-fetching functions, while Saxon adds the XPath 3.1 + * {@code fn:doc}, {@code fn:collection} and {@code fn:unparsed-text} functions that can reach external resources. Rather than branching on the implementation + * class, {@link #harden(XPathFactory)} probes what the factory supports and adapts:

+ *
    + *
  • Saxon ({@code net.sf.saxon}): recognized by class name and handed to {@link SaxonProvider#configure(XPathFactory)}. Its URI-fetching + * 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 TransformerHardener}, kept as a documented class-name 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 sysprop swap to a third-party parser (defense-in-depth); Xalan rejects the feature and is left unchanged.
  • + *
  • FSP ({@link 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.
  • + *
+ */ +final class XPathHardener { + + /** + * Class names of Saxon's {@link XPathFactory} (open-source and commercial editions), hardened through a Saxon {@code Configuration} rather than the standard + * JAXP knobs. The commercial editions subclass the open-source {@code net.sf.saxon.xpath.XPathFactoryImpl}, so {@link SaxonProvider} handles all three. + */ + private static final Set SAXON_XPATH_FACTORIES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList( + "net.sf.saxon.xpath.XPathFactoryImpl", + "com.saxonica.config.ProfessionalXPathFactory", + "com.saxonica.config.EnterpriseXPathFactory"))); + + /** + * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX parser; defense-in-depth against a sysprop swap to a third-party parser. + */ + private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; + + static XPathFactory harden(final XPathFactory factory) { + if (SAXON_XPATH_FACTORIES.contains(factory.getClass().getName())) { + // 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); + return factory; + } + + private XPathHardener() { + } +} \ No newline at end of file diff --git a/src/main/java/org/apache/commons/xml/XalanProvider.java b/src/main/java/org/apache/commons/xml/XalanProvider.java deleted file mode 100644 index 86980de..0000000 --- a/src/main/java/org/apache/commons/xml/XalanProvider.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.commons.xml; - -import static org.apache.commons.xml.JaxpSetters.setFeature; - -import javax.xml.XMLConstants; -import javax.xml.xpath.XPathFactory; - -/** - * Hardening recipes for the external Apache Xalan distribution (the {@code xalan:xalan} artifact). - * - *

Factory classes live under the {@code org.apache.xalan.*} and {@code org.apache.xpath.*} packages. Xalan ships only TrAX and XPath; its DOM, SAX, StAX and - * Schema needs are served by whatever JDK or external Xerces is on the classpath.

- * - *

This class only handles XPath. TrAX hardening is capability-driven across all implementations and lives in {@link TransformerFactoryHardener}, which applies - * to Xalan the same building blocks it applies to XSLTC (FSP, a deny-all {@link javax.xml.transform.URIResolver} for {@code xsl:import}/{@code xsl:include} and - * {@code document()}, and a {@link HardeningTransformerFactory} wrapper so source parsing runs through an {@link XmlFactories}-hardened reader).

- */ -final class XalanProvider { - - static XPathFactory configure(final XPathFactory factory) { - // Required: enables Xalan's secure-processing mode; XPathFactory has no property API for finer control. - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - return factory; - } - - private XalanProvider() { - } -} diff --git a/src/main/java/org/apache/commons/xml/XmlFactories.java b/src/main/java/org/apache/commons/xml/XmlFactories.java index 1815457..41056ff 100644 --- a/src/main/java/org/apache/commons/xml/XmlFactories.java +++ b/src/main/java/org/apache/commons/xml/XmlFactories.java @@ -73,19 +73,6 @@ */ public final class XmlFactories { - private static XPathFactory dispatch(final XPathFactory factory) { - switch (factory.getClass().getName()) { - case "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl": - return StockJdkProvider.configure(factory); - case "org.apache.xpath.jaxp.XPathFactoryImpl": - return XalanProvider.configure(factory); - case "net.sf.saxon.xpath.XPathFactoryImpl": - return SaxonProvider.configure(factory); - default: - throw noProvider(factory); - } - } - /** * Rewrites a {@link Source} so that any SAX parsing it triggers runs through an {@link XmlFactories}-hardened {@link XMLReader}. * @@ -205,15 +192,10 @@ public static XMLInputFactory newXMLInputFactory() { * {@code unparsed-text()}) are not resolved.

* * @return a hardened factory. - * @throws IllegalStateException if the underlying XPath implementation is not recognized by any bundled hardening recipe, or if the matching recipe cannot - * apply its settings to it. + * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. */ public static XPathFactory newXPathFactory() { - return dispatch(XPathFactory.newInstance()); - } - - private static HardeningException noProvider(final Object factory) { - return new HardeningException("No hardening recipe for JAXP factory class " + factory.getClass().getName()); + return XPathHardener.harden(XPathFactory.newInstance()); } private XmlFactories() { From 8acc094a51b1a80c793d38a12eeab1a5d5607145 Mon Sep 17 00:00:00 2001 From: "Piotr P. Karwasz" Date: Mon, 6 Jul 2026 12:18:24 +0200 Subject: [PATCH 3/3] fix: EOF --- src/main/java/org/apache/commons/xml/XPathHardener.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/apache/commons/xml/XPathHardener.java b/src/main/java/org/apache/commons/xml/XPathHardener.java index 488c641..38f3e9d 100644 --- a/src/main/java/org/apache/commons/xml/XPathHardener.java +++ b/src/main/java/org/apache/commons/xml/XPathHardener.java @@ -75,4 +75,4 @@ static XPathFactory harden(final XPathFactory factory) { private XPathHardener() { } -} \ No newline at end of file +}