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:
+ *
+ * - Saxon ({@code net.sf.saxon}): recognized by class name and handed to {@link SaxonProvider#configure(TransformerFactory)}. Unlike XSLTC
+ * and Xalan, Saxon reaches external resources through several channels (the {@link URIResolver}, a collection finder, an unparsed-text resolver) on top
+ * of reflection-based extension functions, none of which the standard JAXP knobs can close; only a locked-down Saxon {@code Configuration} can. This is
+ * the TrAX counterpart of the Android special case in {@link DocumentBuilderHardener}, kept as a documented class-name exception because the required
+ * hardening surface is reachable only through a vendor API.
+ * - FSP ({@link XMLConstants#FEATURE_SECURE_PROCESSING}): required. On XSLTC it enables the runtime evaluator limits; on Xalan it disables
+ * reflection-based extension functions.
+ * - Limits: applied best-effort by {@link Limits#tryApply(TransformerFactory)}. XSLTC honours the JDK attribute limits; Xalan ignores them
+ * (its caps come from FSP).
+ * - {@code ACCESS_EXTERNAL_DTD} (set to {@code ""}): required on XSLTC. XSLTC copies this factory attribute onto the reader that parses the
+ * stylesheet ({@code Util.getInputSource}), overwriting the {@code ACCESS_EXTERNAL_DTD} the wrapper's hardened reader had already set; without it a
+ * permissive default re-opens the external-DTD/entity channel during stylesheet compilation. Xalan rejects the attribute (best-effort, ignored) and closes
+ * that channel through the hardened reader instead. Its sibling {@code ACCESS_EXTERNAL_STYLESHEET} is not set: the deny-all resolver below already
+ * guards the only channel it covers.
+ * - {@link Resolvers.DenyAll#URI}: required. A deny-all {@link URIResolver} blocks {@code xsl:import}/{@code xsl:include} at compile time
+ * and {@code document()} at runtime, the one channel both XSLTC and Xalan route through.
+ * - {@link HardeningTransformerFactory}: required. Both implementations fall back to {@code SAXParserFactory.newInstance()} to parse a
+ * stylesheet or source document that does not carry its own reader, and only set FSP on it; wrapping the factory rewrites every {@link Source} through an
+ * {@link XmlFactories}-hardened reader instead. On Xalan that reader (its deny-all {@link org.xml.sax.EntityResolver} or its own
+ * {@code ACCESS_EXTERNAL_DTD}) is what blocks external DTDs and entities.
+ *
+ *
+ * Caveats
+ *
+ * - Replacing the {@code URIResolver} on the returned factory or on a produced transformer cancels the deny-all block for XSLT URI fetches; neither XSLTC
+ * nor Xalan exposes a tamper-resistant equivalent of JAXP 1.5 {@code ACCESS_EXTERNAL_STYLESHEET}.
+ *
+ */
+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
+}