From 9bc179bf0f42fefae35851f0c754efcf2632d653 Mon Sep 17 00:00:00 2001
From: "Piotr P. Karwasz"
Date: Sun, 28 Jun 2026 21:15:13 +0200
Subject: [PATCH] Base XMLInputFactory hardening on feature support
Replace the per-implementation class-name dispatch for StAX with a single
capability-driven recipe, mirroring the DocumentBuilderFactory and
SAXParserFactory rework. A new StaxHardener consolidates the JDK Zephyr and
Woodstox recipes into one pass that probes which properties each factory
accepts and adapts:
- Limits are applied best-effort by Limits.tryApply(XMLInputFactory), which sets
both the JDK URL limit properties and the Woodstox com.ctc.wstx.* properties;
each implementation honours its own and rejects the other's.
- The external DTD subset is skipped via Zephyr's ignore-external-dtd
(best-effort) so a DOCTYPE-only document still parses without a fetch attempt.
- External entities are denied through resolvers, leaving the standard
SUPPORT_DTD / IS_SUPPORTING_EXTERNAL_ENTITIES defaults untouched. Woodstox
exposes fine-grained hooks (dtdResolver, entityResolver,
undeclaredEntityResolver), so when all three apply the factory is Woodstox;
any factory that does not accept that trio (the JDK Zephyr, or an
unrecognized implementation) instead gets a single deny-all XMLResolver via
setXMLResolver.
JaxpSetters gains trySetProperty(XMLInputFactory) and
setOptionalProperty(XMLInputFactory). Limits.tryApply(XMLInputFactory) replaces
applyToJdkStax and applyToWoodstox. WoodstoxProvider and
StockJdkProvider.configure(XMLInputFactory) are removed;
XmlFactories.newXMLInputFactory() routes through StaxHardener. The DTD_SUBSET_ONLY
resolver moves to StaxHardener, and its SpotBugs known-null exclusion moves with
it.
An implementation is no longer rejected for being unrecognized: it is hardened
best-effort, with the deny-all XMLResolver as the fallback. The throw-on-external
-entity behaviour of both bundled parsers is preserved, so the StAX attack tests
are unchanged.
Assisted-By: Claude Opus 4.8 (1M context)
---
src/conf/spotbugs-exclude-filter.xml | 4 +-
.../org/apache/commons/xml/JaxpSetters.java | 88 +++++++++++-------
.../java/org/apache/commons/xml/Limits.java | 76 ++++++++-------
.../org/apache/commons/xml/StaxHardener.java | 92 +++++++++++++++++++
.../apache/commons/xml/StockJdkProvider.java | 28 +-----
.../apache/commons/xml/WoodstoxProvider.java | 88 ------------------
.../org/apache/commons/xml/XmlFactories.java | 16 +---
7 files changed, 191 insertions(+), 201 deletions(-)
create mode 100644 src/main/java/org/apache/commons/xml/StaxHardener.java
delete mode 100644 src/main/java/org/apache/commons/xml/WoodstoxProvider.java
diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml
index 095ba0e..24038fd 100644
--- a/src/conf/spotbugs-exclude-filter.xml
+++ b/src/conf/spotbugs-exclude-filter.xml
@@ -19,9 +19,9 @@
xmlns="https://github.com/spotbugs/filter/3.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
-
+
-
+
diff --git a/src/main/java/org/apache/commons/xml/JaxpSetters.java b/src/main/java/org/apache/commons/xml/JaxpSetters.java
index 3d2ffec..7941fc8 100644
--- a/src/main/java/org/apache/commons/xml/JaxpSetters.java
+++ b/src/main/java/org/apache/commons/xml/JaxpSetters.java
@@ -37,15 +37,14 @@
*/
final class JaxpSetters {
- private static final String KIND_PROPERTY = "property";
- private static final String KIND_FEATURE = "feature";
- private static final String KIND_ATTRIBUTE = "attribute";
-
/** Action that may throw any exception; used to share a single try/catch around every JAXP setter. */
@FunctionalInterface
private interface ThrowingAction {
void run() throws Exception;
}
+ private static final String KIND_ATTRIBUTE = "attribute";
+ private static final String KIND_FEATURE = "feature";
+ private static final String KIND_PROPERTY = "property";
private static void apply(final Object factory, final String kind, final String name, final ThrowingAction action) {
try {
@@ -59,28 +58,6 @@ static void setAttribute(final DocumentBuilderFactory factory, final String attr
apply(factory, KIND_ATTRIBUTE, attribute, () -> factory.setAttribute(attribute, value));
}
- /**
- * Sets an attribute on a {@link DocumentBuilderFactory} and returns whether the implementation accepted it. Some implementations may reject certain
- * attributes, in which case this method will return {@code false}.
- *
- * @param factory The target factory on which to set the attribute.
- * @param attribute The name of the attribute to set.
- * @param value The value of the attribute to set.
- * @return {@code true} if the attribute was applied, {@code false} if the implementation rejected it.
- */
- static boolean trySetAttribute(final DocumentBuilderFactory factory, final String attribute, final Object value) {
- try {
- factory.setAttribute(attribute, value);
- return true;
- } catch (final Exception e) {
- return false;
- }
- }
-
- static void setOptionalAttribute(final DocumentBuilderFactory factory, final String attribute, final Object value) {
- trySetAttribute(factory, attribute, value);
- }
-
static void setAttribute(final TransformerFactory factory, final String attribute, final Object value) {
apply(factory, KIND_ATTRIBUTE, attribute, () -> factory.setAttribute(attribute, value));
}
@@ -89,14 +66,6 @@ static void setFeature(final DocumentBuilderFactory factory, final String featur
apply(factory, KIND_FEATURE, feature, () -> factory.setFeature(feature, value));
}
- static void setOptionalFeature(final DocumentBuilderFactory 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 setFeature(final SAXParserFactory factory, final String feature, final boolean value) {
apply(factory, KIND_FEATURE, feature, () -> factory.setFeature(feature, value));
}
@@ -125,6 +94,22 @@ static void setFeature(final XMLReader reader, final String feature, final boole
apply(reader, KIND_FEATURE, feature, () -> reader.setFeature(feature, value));
}
+ static void setOptionalAttribute(final DocumentBuilderFactory factory, final String attribute, final Object value) {
+ trySetAttribute(factory, attribute, value);
+ }
+
+ static void setOptionalFeature(final DocumentBuilderFactory 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);
+ }
+
static void setProperty(final XMLInputFactory factory, final String property, final Object value) {
apply(factory, KIND_PROPERTY, property, () -> factory.setProperty(property, value));
}
@@ -149,6 +134,41 @@ static void setProperty(final ValidatorHandler handler, final String property, f
apply(handler, KIND_PROPERTY, property, () -> handler.setProperty(property, value));
}
+ /**
+ * Sets an attribute on a {@link DocumentBuilderFactory} and returns whether the implementation accepted it. Some implementations may reject certain
+ * attributes, in which case this method will return {@code false}.
+ *
+ * @param factory The target factory on which to set the attribute.
+ * @param attribute The name of the attribute to set.
+ * @param value The value of the attribute to set.
+ * @return {@code true} if the attribute was applied, {@code false} if the implementation rejected it.
+ */
+ static boolean trySetAttribute(final DocumentBuilderFactory factory, final String attribute, final Object value) {
+ try {
+ factory.setAttribute(attribute, value);
+ return true;
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Sets a property on an {@link XMLInputFactory} and returns whether the implementation accepted it.
+ *
+ * @param factory The target factory on which to set the property.
+ * @param property The name of the property to set.
+ * @param value The value of the property to set.
+ * @return {@code true} if the property was applied, {@code false} if the implementation rejected it.
+ */
+ static boolean trySetProperty(final XMLInputFactory factory, final String property, final Object value) {
+ try {
+ factory.setProperty(property, value);
+ return true;
+ } catch (final Exception e) {
+ return false;
+ }
+ }
+
private JaxpSetters() {
}
}
diff --git a/src/main/java/org/apache/commons/xml/Limits.java b/src/main/java/org/apache/commons/xml/Limits.java
index d60c655..6b79c23 100644
--- a/src/main/java/org/apache/commons/xml/Limits.java
+++ b/src/main/java/org/apache/commons/xml/Limits.java
@@ -19,6 +19,7 @@
import static org.apache.commons.xml.JaxpSetters.setAttribute;
import static org.apache.commons.xml.JaxpSetters.setOptionalAttribute;
import static org.apache.commons.xml.JaxpSetters.setProperty;
+import static org.apache.commons.xml.JaxpSetters.trySetProperty;
import java.util.Collections;
import java.util.LinkedHashMap;
@@ -235,26 +236,6 @@ final class Limits {
JDK_LIMITS = Collections.unmodifiableMap(map);
}
- /**
- * Best-effort application of the processing limits to a {@link DocumentBuilderFactory}, dispatched on the implementation.
- *
- * External Xerces carries its limits on an {@code org.apache.xerces.util.SecurityManager} instance. Every other implementation (the stock JDK and any
- * future attribute-based parser) takes the JDK limit attributes. Neither path throws if the implementation declines a limit.
- *
- * @param factory The target factory to modify.
- */
- static void tryApply(final DocumentBuilderFactory factory) {
- if (EXTERNAL_XERCES_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
- // Install a fresh SecurityManager pinned to JDK 25 limits, replacing Xerces' built-in caps which are looser than even JDK 8.
- final Object securityManager = newSecurityManager();
- applyToXerces(securityManager);
- setAttribute(factory, XercesProvider.XERCES_SECURITY_MANAGER_PROPERTY, securityManager);
- return;
- }
- // Pin the JDK attribute limits to JDK 25 secure values; skip silently any attribute the implementation does not recognize.
- JDK_LIMITS.forEach((name, supplier) -> setOptionalAttribute(factory, name, Integer.toString(supplier.getAsInt())));
- }
-
/**
* Sets every JDK-supported limit on a stock JDK {@link SchemaFactory}.
*
@@ -264,15 +245,6 @@ static void applyToJdkSchema(final SchemaFactory factory) {
JDK_LIMITS.forEach((name, supplier) -> setProperty(factory, name, Integer.toString(supplier.getAsInt())));
}
- /**
- * Sets every JDK-supported limit on the stock JDK's {@link XMLInputFactory}.
- *
- * @param factory The target factory to modify.
- */
- static void applyToJdkStax(final XMLInputFactory factory) {
- JDK_LIMITS.forEach((name, supplier) -> setProperty(factory, name, Integer.toString(supplier.getAsInt())));
- }
-
/**
* Sets every JDK-supported limit on a stock JDK {@link TransformerFactory}.
*
@@ -291,17 +263,6 @@ static void applyToJdkXmlReader(final XMLReader reader) {
JDK_LIMITS.forEach((name, supplier) -> setProperty(reader, name, Integer.toString(supplier.getAsInt())));
}
- /**
- * Sets every JDK-supported limit on a Woodstox {@link XMLInputFactory}.
- *
- * @param factory The target factory to modify.
- */
- static void applyToWoodstox(final XMLInputFactory factory) {
- setProperty(factory, WSTX_MAX_ENTITY_COUNT, getEntityExpansionLimit());
- setProperty(factory, WSTX_MAX_ATTRIBUTES_PER_ELEMENT, getElementAttributeLimit());
- setProperty(factory, WSTX_MAX_ELEMENT_DEPTH, getMaxElementDepth());
- }
-
/**
* Sets every JDK-supported limit on a Xerces {@code org.apache.xerces.util.SecurityManager}.
*
@@ -376,6 +337,41 @@ private static int read(final String systemPropertyName, final int defaultValue)
}
}
+ /**
+ * Best-effort application of the processing limits to a {@link DocumentBuilderFactory}, dispatched on the implementation.
+ *
+ * External Xerces carries its limits on an {@code org.apache.xerces.util.SecurityManager} instance. Every other implementation (the stock JDK and any
+ * future attribute-based parser) takes the JDK limit attributes. Neither path throws if the implementation declines a limit.
+ *
+ * @param factory The target factory to modify.
+ */
+ static void tryApply(final DocumentBuilderFactory factory) {
+ if (EXTERNAL_XERCES_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) {
+ // Install a fresh SecurityManager pinned to JDK 25 limits, replacing Xerces' built-in caps which are looser than even JDK 8.
+ final Object securityManager = newSecurityManager();
+ applyToXerces(securityManager);
+ setAttribute(factory, XercesProvider.XERCES_SECURITY_MANAGER_PROPERTY, securityManager);
+ return;
+ }
+ // Pin the JDK attribute limits to JDK 25 secure values; skip silently any attribute the implementation does not recognize.
+ 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.
+ *
+ * The JDK's Zephyr honours the JDK URL limit properties; Woodstox honours its own {@code com.ctc.wstx.*} properties. Each implementation rejects the
+ * other's, so both sets are applied best-effort and the rejected ones are skipped silently.
+ *
+ * @param factory The target factory to modify.
+ */
+ static void tryApply(final XMLInputFactory factory) {
+ JDK_LIMITS.forEach((name, supplier) -> trySetProperty(factory, name, Integer.toString(supplier.getAsInt())));
+ trySetProperty(factory, WSTX_MAX_ENTITY_COUNT, getEntityExpansionLimit());
+ trySetProperty(factory, WSTX_MAX_ATTRIBUTES_PER_ELEMENT, getElementAttributeLimit());
+ trySetProperty(factory, WSTX_MAX_ELEMENT_DEPTH, getMaxElementDepth());
+ }
+
private Limits() {
}
}
diff --git a/src/main/java/org/apache/commons/xml/StaxHardener.java b/src/main/java/org/apache/commons/xml/StaxHardener.java
new file mode 100644
index 0000000..378edc1
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/StaxHardener.java
@@ -0,0 +1,92 @@
+/*
+ * 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.setOptionalProperty;
+import static org.apache.commons.xml.JaxpSetters.trySetProperty;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLStreamException;
+
+/**
+ * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath.
+ *
+ * Rather than branching on the implementation class, {@link #harden(XMLInputFactory)} consolidates the JDK Zephyr and Woodstox recipes into one pass that
+ * probes which properties each factory accepts and adapts:
+ *
+ * - Limits: applied best-effort by {@link Limits#tryApply(XMLInputFactory)}, which sets both the JDK and the Woodstox limit properties;
+ * each implementation honours its own and rejects the other's.
+ * - External DTD subset: skipped via Zephyr's {@value #ZEPHYR_IGNORE_EXTERNAL_DTD} (best-effort), so a DOCTYPE-only document parses
+ * without a fetch attempt instead of tripping the deny-all resolver below. Woodstox skips it through {@value #WSTX_DTD_RESOLVER} instead.
+ * - External entities: denied through resolvers, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES}
+ * defaults untouched. Woodstox exposes fine-grained hooks, so when all three apply the factory is Woodstox: {@value #WSTX_DTD_RESOLVER} (empty external
+ * subset, but a thrown error on external parameter entities, which share that hook), {@value #WSTX_ENTITY_RESOLVER} (throw on declared external general
+ * entities) and {@value #WSTX_UNDECLARED_ENTITY_RESOLVER} (silently drop undeclared references left by the skipped subset). Any factory that does not
+ * accept that trio (the JDK Zephyr, or an unrecognized implementation) instead gets a single deny-all {@link Resolvers.DenyAll#XML} through
+ * {@code setXMLResolver}.
+ *
+ */
+final class StaxHardener {
+
+ /** Zephyr property: skip external DTD subset loading entirely, so a DOCTYPE-only document parses without a fetch attempt. */
+ private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = "http://java.sun.com/xml/stream/properties/ignore-external-dtd";
+
+ /** Woodstox property: resolver consulted for the external DTD subset. */
+ private static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
+
+ /** Woodstox property: resolver consulted for declared external general entities. */
+ private static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
+
+ /** Woodstox property: resolver consulted for undeclared entity references. */
+ private static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
+
+ /**
+ * Hybrid Woodstox DTD resolver: returns the empty input for the external DTD subset, throws on external parameter entities.
+ *
+ * Woodstox calls this hook with {@code entityName == null} for the subset and {@code entityName != null} for parameter-entity expansion; that
+ * discriminator is Woodstox-specific (the JDK Zephyr's {@code XMLResolver} always receives {@code null} as the 4th argument), so the resolver lives
+ * here and is applied best-effort, ignored by implementations that do not recognize the property.
+ */
+ static final XMLResolver DTD_SUBSET_ONLY = (publicID, systemID, baseURI, entityName) -> {
+ if (entityName != null) {
+ throw new XMLStreamException("External parameter entity '" + entityName + "' refused (publicID=" + publicID + ", systemID=" + systemID
+ + ", baseURI=" + baseURI + ")");
+ }
+ return Resolvers.IgnoreAll.XML.resolveEntity(publicID, systemID, baseURI, entityName);
+ };
+
+ static XMLInputFactory harden(final XMLInputFactory factory) {
+ // Optional, implementation-based: JDK limit properties or Woodstox limit properties.
+ Limits.tryApply(factory);
+ // Optional: Zephyr's StAX equivalent of XERCES_LOAD_EXTERNAL_DTD=false skips the external DTD subset entirely.
+ setOptionalProperty(factory, ZEPHYR_IGNORE_EXTERNAL_DTD, true);
+
+ // Woodstox-specific fine-grained resolvers
+ if (!(trySetProperty(factory, WSTX_DTD_RESOLVER, DTD_SUBSET_ONLY)
+ && trySetProperty(factory, WSTX_ENTITY_RESOLVER, Resolvers.DenyAll.XML)
+ && trySetProperty(factory, WSTX_UNDECLARED_ENTITY_RESOLVER, Resolvers.IgnoreAll.XML))) {
+ // Fallback: use deny-all resolver
+ factory.setXMLResolver(Resolvers.DenyAll.XML);
+ }
+ return factory;
+ }
+
+ private StaxHardener() {
+ }
+}
diff --git a/src/main/java/org/apache/commons/xml/StockJdkProvider.java b/src/main/java/org/apache/commons/xml/StockJdkProvider.java
index 99a7d8d..4ffc40a 100644
--- a/src/main/java/org/apache/commons/xml/StockJdkProvider.java
+++ b/src/main/java/org/apache/commons/xml/StockJdkProvider.java
@@ -23,7 +23,6 @@
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
-import javax.xml.stream.XMLInputFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.validation.SchemaFactory;
@@ -42,17 +41,15 @@
* 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*}: required on {@link XMLInputFactory} (it rejects FSP); elsewhere defense-in-depth, pinning the limits to
- * JDK 25 secure values so older JDKs do not fall back to looser defaults.
+ * {@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. {@link XMLInputFactory} has no equivalent property, so the StAX path pairs Zephyr's
- * {@value #ZEPHYR_IGNORE_EXTERNAL_DTD} property (skip the external DTD subset, lets DOCTYPE-only documents parse) with {@link Resolvers.DenyAll#XML}
- * (throw on declared external entity references). Undeclared general-entity references are silently dropped: Zephyr does not raise a fatal error
- * when the subset that would have declared the entity was skipped, so no extra hook is needed.
+ * javax.xml.accessExternal*}) cannot loosen them.
*
*
* SAX hardening lives in {@link #configure(XMLReader)}: {@link SAXParserFactory} has no property API, so the {@link HardeningSAXParserFactory} wrapper
- * funnels each produced parser's {@link XMLReader} through that method.
+ * funnels each produced parser's {@link XMLReader} through that method. StAX hardening is capability-driven across all implementations and lives in
+ * {@link StaxHardener}.
*/
final class StockJdkProvider {
@@ -66,11 +63,6 @@ final class StockJdkProvider {
*/
private static final String XERCES_LOAD_EXTERNAL_DTD = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
- /**
- * Zephyr property: skip external DTD subset loading entirely (StAX equivalent of {@link #XERCES_LOAD_EXTERNAL_DTD} {@code = false}).
- */
- private static final String ZEPHYR_IGNORE_EXTERNAL_DTD = "http://java.sun.com/xml/stream/properties/ignore-external-dtd";
-
static SAXParserFactory configure(final SAXParserFactory factory) {
// Required: enables the JDK XMLSecurityManager limits.
setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
@@ -93,16 +85,6 @@ static XMLReader configure(final XMLReader reader) {
return reader;
}
- static XMLInputFactory configure(final XMLInputFactory factory) {
- // Required: XMLInputFactory rejects FSP, so the limits below are the only way to enable JDK XMLSecurityManager caps on the StAX path.
- Limits.applyToJdkStax(factory);
- // Let DOCTYPE-only documents parse silently: Zephyr's StAX equivalent of XERCES_LOAD_EXTERNAL_DTD=false skips the external DTD subset entirely.
- factory.setProperty(ZEPHYR_IGNORE_EXTERNAL_DTD, true);
- // Required: XMLInputFactory has no ACCESS_EXTERNAL_* either; an explicit deny-all resolver is the only way to block external entity fetching.
- factory.setXMLResolver(Resolvers.DenyAll.XML);
- return factory;
- }
-
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);
diff --git a/src/main/java/org/apache/commons/xml/WoodstoxProvider.java b/src/main/java/org/apache/commons/xml/WoodstoxProvider.java
deleted file mode 100644
index c843615..0000000
--- a/src/main/java/org/apache/commons/xml/WoodstoxProvider.java
+++ /dev/null
@@ -1,88 +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 javax.xml.stream.XMLInputFactory;
-import javax.xml.stream.XMLResolver;
-import javax.xml.stream.XMLStreamException;
-
-/**
- * Hardening recipe for the FasterXML Woodstox StAX implementation ({@code com.ctc.wstx:woodstox-core}).
- *
- * Woodstox is a StAX-only library, so this class only handles {@link XMLInputFactory}.
- *
- * Hardening recipe used below:
- *
- * - {@link Limits#applyToWoodstox}: defense-in-depth. Woodstox already enforces its own caps (see {@code ReaderConfig.DEFAULT_*}), but
- * they are looser than the JDK 25 secure values; this call aligns them so a Woodstox-backed factory enforces the same processing limits as the JDK
- * parsers.
- * - Three resolver hooks: required. Woodstox honours {@code IS_SUPPORTING_EXTERNAL_ENTITIES=true} and {@code SUPPORT_DTD=true} by
- * default and routes lookups through three Woodstox-specific resolver properties; the hardened factory installs:
- *
- * - {@code com.ctc.wstx.dtdResolver} = {@link #DTD_SUBSET_ONLY}: returns an empty input for the external DTD subset so DOCTYPE-only
- * documents parse silently, but throws on external parameter entities (which share this hook). The {@code entityName} 4th argument is
- * the discriminator: {@code null} for the subset, the entity name for parameter entities (see {@code DefaultInputResolver} and
- * {@code ValidatingStreamReader.findDtdExtSubset}).
- * - {@code com.ctc.wstx.entityResolver} = {@link Resolvers.DenyAll#XML}: throws on declared external general entities.
- * - {@code com.ctc.wstx.undeclaredEntityResolver} = {@link Resolvers.IgnoreAll#XML}: silently drops references to entities the parser has not
- * seen declared, matching the SAX path's behaviour.
- *
- *
- *
- */
-final class WoodstoxProvider {
-
- /** Woodstox property: resolver consulted for the external DTD subset and for external parameter entities. */
- private static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver";
-
- /** Woodstox property: resolver consulted for declared external general entities. */
- private static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver";
-
- /** Woodstox property: resolver consulted for undeclared entity references. */
- private static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver";
-
- /**
- * Hybrid Woodstox DTD resolver: returns the empty input for the external DTD subset, throws on external parameter entities.
- *
- * Woodstox calls this hook with {@code entityName == null} for the subset and {@code entityName != null} for parameter-entity expansion; that
- * discriminator is Woodstox-specific (the JDK Zephyr's {@code XMLResolver} always receives {@code null} as the 4th argument), so the resolver lives
- * here rather than in {@link Resolvers}.
- */
- static final XMLResolver DTD_SUBSET_ONLY = (publicID, systemID, baseURI, entityName) -> {
- if (entityName != null) {
- throw new XMLStreamException("External parameter entity '" + entityName + "' refused (publicID=" + publicID + ", systemID=" + systemID
- + ", baseURI=" + baseURI + ")");
- }
- return Resolvers.IgnoreAll.XML.resolveEntity(publicID, systemID, baseURI, entityName);
- };
-
- static XMLInputFactory configure(final XMLInputFactory factory) {
- // Defense-in-depth: align Woodstox's built-in caps with the JDK 25 secure values; Woodstox's own defaults are functional but looser.
- Limits.applyToWoodstox(factory);
- // Required: empty external subset, throw on external parameter entities.
- factory.setProperty(WSTX_DTD_RESOLVER, DTD_SUBSET_ONLY);
- // Required: throw on declared external general entities.
- factory.setProperty(WSTX_ENTITY_RESOLVER, Resolvers.DenyAll.XML);
- // Required: silently drop undeclared entity references, matching the SAX path's tolerance.
- factory.setProperty(WSTX_UNDECLARED_ENTITY_RESOLVER, Resolvers.IgnoreAll.XML);
- return factory;
- }
-
- private WoodstoxProvider() {
- }
-}
diff --git a/src/main/java/org/apache/commons/xml/XmlFactories.java b/src/main/java/org/apache/commons/xml/XmlFactories.java
index e7a7953..44b2e44 100644
--- a/src/main/java/org/apache/commons/xml/XmlFactories.java
+++ b/src/main/java/org/apache/commons/xml/XmlFactories.java
@@ -86,17 +86,6 @@ private static SAXParserFactory dispatch(final SAXParserFactory factory) {
}
}
- private static XMLInputFactory dispatch(final XMLInputFactory factory) {
- switch (factory.getClass().getName()) {
- case "com.sun.xml.internal.stream.XMLInputFactoryImpl":
- return StockJdkProvider.configure(factory);
- case "com.ctc.wstx.stax.WstxInputFactory":
- return WoodstoxProvider.configure(factory);
- default:
- throw noProvider(factory);
- }
- }
-
private static TransformerFactory dispatch(final TransformerFactory factory) {
switch (factory.getClass().getName()) {
case "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl":
@@ -254,11 +243,10 @@ public static TransformerFactory newTransformerFactory() {
* The three universal guarantees on {@link XmlFactories} apply; StAX exposes no additional vectors beyond them.
*
* @return a hardened factory.
- * @throws IllegalStateException if the underlying StAX 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 XMLInputFactory newXMLInputFactory() {
- return dispatch(XMLInputFactory.newInstance());
+ return StaxHardener.harden(XMLInputFactory.newInstance());
}
/**