diff --git a/src/main/java/org/apache/commons/xml/JaxpSetters.java b/src/main/java/org/apache/commons/xml/JaxpSetters.java index b73a6de..6e056dd 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); @@ -113,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/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 deleted file mode 100644 index efb8839..0000000 --- a/src/main/java/org/apache/commons/xml/StockJdkProvider.java +++ /dev/null @@ -1,78 +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.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; - -/** - * 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:

- * - */ -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 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); - // 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/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/XPathHardener.java b/src/main/java/org/apache/commons/xml/XPathHardener.java new file mode 100644 index 0000000..38f3e9d --- /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:

+ * + */ +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() { + } +} 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 21a3f7c..0000000 --- a/src/main/java/org/apache/commons/xml/XalanProvider.java +++ /dev/null @@ -1,73 +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.transform.TransformerFactory; -import javax.xml.transform.sax.SAXTransformerFactory; -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.

- * - *

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

- * - * - *

Caveats

- * - */ -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); - 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 ca1a9c6..41056ff 100644 --- a/src/main/java/org/apache/commons/xml/XmlFactories.java +++ b/src/main/java/org/apache/commons/xml/XmlFactories.java @@ -73,35 +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": - 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}. * @@ -196,11 +167,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()); } /** @@ -222,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() {