This is an automated email from the ASF dual-hosted git repository. ppkarwasz pushed a commit to branch feature/merge-hardeners-into-factories in repository https://gitbox.apache.org/repos/asf/commons-xml.git
commit d401123fcbb419106203e1cc2a5f979da1084276 Author: Piotr P. Karwasz <[email protected]> AuthorDate: Thu Aug 27 21:25:10 2026 +0200 Merge the static hardening recipes into the public factory classes Each public Hardening*Factory absorbs its package-private hardener (DocumentBuilderHardener and siblings): the harden methods, helpers and constants move onto the entry point, the hardener files are deleted, and the wrap methods are removed since each wrapper is now constructed from its own class only. On HardeningSAXParserFactory the reader and source variants become harden(XMLReader) and harden(Source) overloads. Assisted-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01WWJ4LAxx3TX5RwNvwKbAS3 --- src/conf/spotbugs-exclude-filter.xml | 6 - .../commons/xml/DocumentBuilderHardener.java | 86 ---------- .../commons/xml/FallbackIgnoreURIResolver.java | 2 +- .../xml/HardeningDocumentBuilderFactory.java | 54 ++++++- .../org/apache/commons/xml/HardeningSAXParser.java | 2 +- .../commons/xml/HardeningSAXParserFactory.java | 160 ++++++++++++++++-- .../org/apache/commons/xml/HardeningSchema.java | 2 +- .../apache/commons/xml/HardeningSchemaFactory.java | 35 ++-- .../apache/commons/xml/HardeningTransformer.java | 4 +- .../commons/xml/HardeningTransformerFactory.java | 82 ++++++---- .../org/apache/commons/xml/HardeningValidator.java | 4 +- .../org/apache/commons/xml/HardeningXMLFilter.java | 2 +- .../commons/xml/HardeningXMLInputFactory.java | 42 +++-- .../org/apache/commons/xml/HardeningXPath.java | 2 +- .../apache/commons/xml/HardeningXPathFactory.java | 78 ++++++++- .../org/apache/commons/xml/SAXParserHardener.java | 180 --------------------- .../java/org/apache/commons/xml/SaxonProvider.java | 6 +- .../org/apache/commons/xml/SchemaHardener.java | 53 ------ .../java/org/apache/commons/xml/StaxHardener.java | 47 ------ .../apache/commons/xml/TransformerHardener.java | 73 --------- .../java/org/apache/commons/xml/XPathHardener.java | 106 ------------ .../org/apache/commons/xml/AttackTestSupport.java | 12 +- .../commons/xml/SaxonAlternateFactoryTest.java | 2 +- .../apache/commons/xml/ShadingFootprintTest.java | 89 +++++----- .../xml/UnsupportedXmlImplementationTest.java | 4 +- .../java/org/apache/commons/xml/XIncludeTest.java | 6 +- 26 files changed, 433 insertions(+), 706 deletions(-) diff --git a/src/conf/spotbugs-exclude-filter.xml b/src/conf/spotbugs-exclude-filter.xml index 24038fd..5ee3d92 100644 --- a/src/conf/spotbugs-exclude-filter.xml +++ b/src/conf/spotbugs-exclude-filter.xml @@ -19,10 +19,4 @@ 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"> - <!-- Looks like an NPE: the DTD_SUBSET_ONLY lambda forwards a known-null entityName to the ignore-all resolver. --> - <Match> - <Class name="org.apache.commons.xml.StaxHardener" /> - <Method name="lambda$static$0" /> - <Bug pattern="NP_LOAD_OF_KNOWN_NULL_VALUE" /> - </Match> </FindBugsFilter> diff --git a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java b/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java deleted file mode 100644 index 6e2563e..0000000 --- a/src/main/java/org/apache/commons/xml/DocumentBuilderHardener.java +++ /dev/null @@ -1,86 +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.XMLConstants; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; - -import org.xml.sax.EntityResolver; - -/** - * Capability-driven hardening for any {@link DocumentBuilderFactory} on the classpath. - * - * <p>Rather than branching on the implementation class, {@link #harden(DocumentBuilderFactory)} probes what the factory supports and adapts:</p> - * <ul> - * <li><strong>Android</strong> (Harmony / KXmlParser): recognized by class name and left untouched. It exposes no {@link XMLConstants#FEATURE_SECURE_PROCESSING - * FSP}, no JAXP 1.5 {@code ACCESS_EXTERNAL_*} and no attribute API at all, while KXmlParser silently drops user-defined entities, so there is nothing to - * apply.</li> - * <li><strong>FSP</strong>: required. It switches on the implementation's built-in security manager, which is what carries the processing limits.</li> - * <li><strong>Ignore-all resolver floor</strong>: every produced {@link DocumentBuilder} is wrapped by a {@link HardeningDocumentBuilderFactory} that keeps an - * ignore-all {@link EntityResolver} floor. That floor blocks external DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's - * XInclude processor ignores {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} attributes are - * needed here. A caller can chain its own resolver onto the floor to allow-list resources, but cannot remove it.</li> - * </ul> - */ -final class DocumentBuilderHardener { - - /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */ - private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl"; - - /** - * Hardens the given factory, returning a hardened wrapper if necessary. - * - * @param factory The factory to harden. - * @return A new hardened factory or the original factory, as-is, if it is a known Android factory. - * @throws HardeningException Thrown if a (non-Andoid) factory cannot support the secure processing feature {@link XMLConstants#FEATURE_SECURE_PROCESSING}. - */ - static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { - // Android exposes no FSP, ACCESS_EXTERNAL_* or attribute API, and KXmlParser drops user-defined entities; nothing to apply. - if (ANDROID_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) { - return factory; - } - // Required: enables the implementation's security manager, which carries the limits. - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - // Required: HardeningDocumentBuilderFactory installs an ignore-all EntityResolver floor on every DocumentBuilder. - // That floor blocks external DTD, entity, schema and xi:include fetches in one place: no ACCESS_EXTERNAL_* attributes are needed here. - // Callers can chain their resolvers, but not override the floor. - return HardeningDocumentBuilderFactory.wrap(factory); - } - - /** - * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. - * - * @param factory The factory to harden. - * @param feature The feature to set. - * @param value The value to set. - * @throws HardeningException Thrown if this factory or the {@code XPath}s it creates cannot support this feature. - * @throws NullPointerException Thrown if the {@code feature} parameter is null. - */ - private static void setFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final ParserConfigurationException e) { - throw HardeningException.settingFailed("feature", feature, factory, e); - } - } - - private DocumentBuilderHardener() { - } -} diff --git a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java index 30f9eec..cf3e920 100644 --- a/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java +++ b/src/main/java/org/apache/commons/xml/FallbackIgnoreURIResolver.java @@ -116,7 +116,7 @@ public Source resolve(final String href, final String base) throws TransformerEx final Source resolved = delegate != null ? delegate.resolve(href, base) : null; if (resolved != null) { // The implementation parses the opted-in handle with an internal reader at its own defaults; the rewrite hands it a hardened reader instead. - return SAXParserHardener.hardenSource(resolved); + return HardeningSAXParserFactory.harden(resolved); } if (HardeningException.throwOnUnresolved()) { throw new TransformerException(HardeningException.forbidden("uri", null, null, href, base)); diff --git a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java index 1888f3c..1e88134 100644 --- a/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningDocumentBuilderFactory.java @@ -39,6 +39,41 @@ */ public final class HardeningDocumentBuilderFactory { + /** Class name of Android's Harmony-based {@link DocumentBuilderFactory}, which exposes no hardening surface. */ + private static final String ANDROID_DOCUMENT_BUILDER_FACTORY = "org.apache.harmony.xml.parsers.DocumentBuilderFactoryImpl"; + + /** + * Capability-driven hardening for any {@link DocumentBuilderFactory} on the classpath. + * + * <p>Rather than branching on the implementation class, this method probes what the factory supports and adapts:</p> + * <ul> + * <li><strong>Android</strong> (Harmony / KXmlParser): recognized by class name and left untouched. It exposes no {@link XMLConstants#FEATURE_SECURE_PROCESSING + * FSP}, no JAXP 1.5 {@code ACCESS_EXTERNAL_*} and no attribute API at all, while KXmlParser silently drops user-defined entities, so there is nothing to + * apply.</li> + * <li><strong>FSP</strong>: required. It switches on the implementation's built-in security manager, which is what carries the processing limits.</li> + * <li><strong>Ignore-all resolver floor</strong>: every produced {@link DocumentBuilder} is wrapped by the nested wrapper, which keeps an + * ignore-all {@link EntityResolver} floor. That floor blocks external DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's + * XInclude processor ignores {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} attributes are + * needed here. A caller can chain its own resolver onto the floor to allow-list resources, but cannot remove it.</li> + * </ul> + * + * @param factory The factory to harden. + * @return A new hardened factory or the original factory, as-is, if it is a known Android factory. + * @throws HardeningException Thrown if a (non-Andoid) factory cannot support the secure processing feature {@link XMLConstants#FEATURE_SECURE_PROCESSING}. + */ + static DocumentBuilderFactory harden(final DocumentBuilderFactory factory) { + // Android exposes no FSP, ACCESS_EXTERNAL_* or attribute API, and KXmlParser drops user-defined entities; nothing to apply. + if (ANDROID_DOCUMENT_BUILDER_FACTORY.equals(factory.getClass().getName())) { + return factory; + } + // Required: enables the implementation's security manager, which carries the limits. + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Required: the wrapper installs an ignore-all EntityResolver floor on every DocumentBuilder. + // That floor blocks external DTD, entity, schema and xi:include fetches in one place: no ACCESS_EXTERNAL_* attributes are needed here. + // Callers can chain their resolvers, but not override the floor. + return new Wrapper(factory); + } + /** * Returns a new, hardened {@link DocumentBuilderFactory}. * <p> @@ -57,17 +92,24 @@ public final class HardeningDocumentBuilderFactory { * implementation is not available or cannot be instantiated. */ public static DocumentBuilderFactory newInstance() { - return DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + return harden(DocumentBuilderFactory.newInstance()); } /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. + * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. + * @param factory The factory to harden. + * @param feature The feature to set. + * @param value The value to set. + * @throws HardeningException Thrown if this factory or the {@code XPath}s it creates cannot support this feature. + * @throws NullPointerException Thrown if the {@code feature} parameter is null. */ - static DocumentBuilderFactory wrap(final DocumentBuilderFactory delegate) { - return new Wrapper(delegate); + private static void setFeature(final DocumentBuilderFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final ParserConfigurationException e) { + throw HardeningException.settingFailed("feature", feature, factory, e); + } } private HardeningDocumentBuilderFactory() { diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java index 8abdc8a..162332c 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParser.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParser.java @@ -81,7 +81,7 @@ public Schema getSchema() { @Override public XMLReader getXMLReader() throws SAXException { if (hardenedReader == null) { - hardenedReader = SAXParserHardener.hardenReader(delegate.getXMLReader()); + hardenedReader = HardeningSAXParserFactory.harden(delegate.getXMLReader()); } return hardenedReader; } diff --git a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java index d45581f..27d91db 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSAXParserFactory.java @@ -19,12 +19,19 @@ import java.util.Objects; +import javax.xml.XMLConstants; import javax.xml.parsers.FactoryConfigurationError; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; +import javax.xml.transform.Source; +import javax.xml.transform.TransformerConfigurationException; +import javax.xml.transform.sax.SAXSource; +import javax.xml.transform.stream.StreamSource; import javax.xml.validation.Schema; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.xml.sax.SAXNotRecognizedException; import org.xml.sax.SAXNotSupportedException; @@ -41,6 +48,108 @@ */ public final class HardeningSAXParserFactory { + /** Class name of Android's Expat-backed {@link XMLReader}. */ + private static final String ANDROID_EXPAT_READER = "org.apache.harmony.xml.ExpatReader"; + /** Class name of Android's Harmony-based {@link SAXParserFactory}, backed by the native Expat parser. */ + private static final String ANDROID_SAX_PARSER_FACTORY = "org.apache.harmony.xml.parsers.SAXParserFactoryImpl"; + + /** + * Capability-driven hardening for any {@link SAXParserFactory} on the classpath. + * + * <p>Rather than branching on the implementation class, this method probes what the parser supports and adapts. Because + * {@link SAXParserFactory} exposes only a feature API and no property API, the per-parse configuration runs on each {@link XMLReader} the factory produces, + * funnelled through the nested wrapper into {@link #harden(XMLReader)}:</p> + * <ul> + * <li><strong>Android</strong> (Harmony / Expat): {@link XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties + * are not recognized, and libexpat enforces its own Billion Laughs check, so neither is applied. Two fixups are still needed: an ignore-all resolver + * (Expat ignores external fetches silently when no resolver is set; the floor keeps that behavior non-bypassable, resolving anything unresolved to + * empty), and a {@link HardeningExpatXMLReader} so the unsupported {@code namespace-prefixes} feature is rejected at + * configuration time rather than mid-parse.</li> + * <li><strong>FSP</strong>: required on every other reader. It switches on the implementation's built-in security manager, which is what carries the + * processing limits.</li> + * <li><strong>Ignore-all resolver floor</strong>: every reader is wrapped in a {@link HardeningXMLReader} that keeps an ignore-all {@link EntityResolver} floor. + * That floor blocks external DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's XInclude processor ignores + * {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} properties are needed here. A caller can + * chain its own resolver onto the floor to allow-list resources, but cannot remove it.</li> + * </ul> + * + * @param factory the factory to harden; never {@code null}. + * @return a hardened factory. + */ + static SAXParserFactory harden(final SAXParserFactory factory) { + // Required: enables the implementation's security manager, which carries the limits. Android's Expat rejects FSP, so it is skipped there. + if (!ANDROID_SAX_PARSER_FACTORY.equals(factory.getClass().getName())) { + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + } + // The per-parse hardening (limits, entity blocking, Android fixups) lives in harden(XMLReader) because SAXParserFactory has no property API. + return new Wrapper(factory); + } + + /** + * Rewrites a {@link Source} so that any SAX parsing it triggers runs through a hardened {@link XMLReader}. + * <p> + * Only a {@link StreamSource} or a {@link SAXSource} without a reader is enriched with a hardened, namespace-aware reader; other source kinds are returned + * as-is. Used by the TrAX and schema wrappers to route every source they parse through the SAX hardening path. + * </p> + * + * @param source the source to harden; never {@code null}. + * @return a hardened source. + * @throws TransformerConfigurationException if a hardened reader cannot be obtained. + * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service + * configuration error} or if the implementation is not available or cannot be instantiated. + */ + static Source harden(final Source source) throws TransformerConfigurationException { + if (source instanceof StreamSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() == null) { + final InputSource inputSource = SAXSource.sourceToInputSource(source); + return inputSource == null ? source : new SAXSource(newHardenedReader(), inputSource); + } + return source; + } + + /** + * Hardens an existing {@link XMLReader}. + * + * @param reader The reader to harden; never {@code null}. + * @return A hardened reader. + * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. + */ + static XMLReader harden(final XMLReader reader) { + if (reader instanceof HardeningXMLReader) { + // Already hardened (for example, a reader from a hardened factory passed back through harden(XMLReader)); the floor is already in place. + return reader; + } + if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) { + // Expat ignores external fetches when no resolver is set; the ignore-all floor keeps that behavior non-bypassable (routing a caller-set resolver, + // including SAXParser.parse's handler, through it and resolving anything unresolved to empty) and, via HardeningExpatXMLReader, rejects the + // unsupported namespace-prefixes feature eagerly rather than mid-parse. + return new HardeningExpatXMLReader(reader); + } + // Required: enables the JDK XMLSecurityManager / Xerces SecurityManager limits. + setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Required: HardeningXMLReader installs an ignore-all EntityResolver floor on the reader. + // That floor blocks external DTD, entity, schema and xi:include fetches in one place: no ACCESS_EXTERNAL_* properties are needed here. + // Callers can chain their resolvers, but not override the floor. + return new HardeningXMLReader(reader); + } + + /** + * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. + * + * @return a hardened reader. + * @throws TransformerConfigurationException if a hardened reader cannot be obtained. + * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service + * configuration error} or if the implementation is not available or cannot be instantiated. + */ + static XMLReader newHardenedReader() throws TransformerConfigurationException { + try { + final SAXParserFactory factory = harden(SAXParserFactory.newInstance()); + factory.setNamespaceAware(true); + return factory.newSAXParser().getXMLReader(); + } catch (final ParserConfigurationException | SAXException e) { + throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e); + } + } + /** * Returns a new, hardened {@link SAXParserFactory}. * <p> @@ -56,17 +165,23 @@ public final class HardeningSAXParserFactory { * error} or if the implementation is not available or cannot be instantiated. */ public static SAXParserFactory newInstance() { - return SAXParserHardener.harden(SAXParserFactory.newInstance()); + return harden(SAXParserFactory.newInstance()); } - /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. - * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. - */ - static SAXParserFactory wrap(final SAXParserFactory delegate) { - return new Wrapper(delegate); + private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final Exception e) { + throw HardeningException.settingFailed("feature", feature, factory, e); + } + } + + private static void setFeature(final XMLReader reader, final String feature, final boolean value) { + try { + reader.setFeature(feature, value); + } catch (final Exception e) { + throw HardeningException.settingFailed("feature", feature, reader, e); + } } private HardeningSAXParserFactory() { @@ -74,7 +189,32 @@ private HardeningSAXParserFactory() { } /** - * Universal SAX factory wrapper that funnels every produced parser through {@link SAXParserHardener#hardenReader(XMLReader)}. + * {@link HardeningXMLReader} for Android's {@code org.apache.harmony.xml.ExpatReader} that additionally surfaces its {@code namespace-prefixes} limitation at + * configuration time. + * + * <p>ExpatReader does not actually support the {@code namespace-prefixes} feature: enabling it is accepted by {@code setFeature} but fails later, during + * {@code parse}, with a {@link SAXNotSupportedException}. Reporting the rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that probe + * the feature, such as Xalan's identity transformer, catch the exception and fall back instead of failing the whole parse.</p> + */ + static final class HardeningExpatXMLReader extends HardeningXMLReader { + + private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes"; + + HardeningExpatXMLReader(final XMLReader delegate) { + super(delegate); + } + + @Override + public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { + if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) { + throw new SAXNotSupportedException("ExpatReader does not support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature"); + } + super.setFeature(name, value); + } + } + + /** + * Universal SAX factory wrapper that funnels every produced parser through {@link HardeningSAXParserFactory#harden(XMLReader)}. * <p> * {@link SAXParserFactory} exposes only a feature API and no property API, so the per-parse hardening (limits, entity blocking, implementation-specific fixups) * has to run on each {@link XMLReader} the factory produces. This wrapper returns a {@link HardeningSAXParser}, which applies that hardening lazily to both the diff --git a/src/main/java/org/apache/commons/xml/HardeningSchema.java b/src/main/java/org/apache/commons/xml/HardeningSchema.java index fe2166d..e92374c 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchema.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchema.java @@ -25,7 +25,7 @@ /** * {@link Schema} wrapper that hardens every {@link Validator} and {@link ValidatorHandler} the inner Schema produces: each {@link Validator} is wrapped in - * {@link HardeningValidator} (which rewrites the Source through {@link SAXParserHardener#hardenSource(javax.xml.transform.Source)} and installs the resolver + * {@link HardeningValidator} (which rewrites the Source through {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source)} and installs the resolver * floor), and each {@link ValidatorHandler} is wrapped in a {@link HardeningValidatorHandler} that keeps the same ignore-all resolver floor so * {@code xsi:schemaLocation} is not resolved during SAX-driven validation. */ diff --git a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java index 2f2f9dc..a123b4a 100644 --- a/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningSchemaFactory.java @@ -44,6 +44,21 @@ */ public final class HardeningSchemaFactory { + /** + * Hardening for any {@link SchemaFactory} on the classpath. + * + * <p>Unlike the other factory types there is no per-implementation branching and no feature or limit configuration on the factory itself: schema compilation + * and validation reach external resources only through the resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on + * every implementation. The reader used to parse schema and instance documents is hardened separately, through + * {@link HardeningSAXParserFactory#harden(javax.xml.transform.Source)}.</p> + * + * @param factory the factory to harden; never {@code null}. + * @return a hardened factory. + */ + static SchemaFactory harden(final SchemaFactory factory) { + return new Wrapper(factory); + } + /** * Returns a new, hardened {@link SchemaFactory} for the given schema language. * <p> @@ -65,17 +80,7 @@ public final class HardeningSchemaFactory { * @throws SchemaFactoryConfigurationError Thrown if a configuration error is encountered. */ public static SchemaFactory newInstance(final String schemaLanguage) { - return new Wrapper(SchemaFactory.newInstance(schemaLanguage)); - } - - /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. - * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. - */ - static SchemaFactory wrap(final SchemaFactory delegate) { - return new Wrapper(delegate); + return harden(SchemaFactory.newInstance(schemaLanguage)); } private HardeningSchemaFactory() { @@ -91,14 +96,14 @@ private HardeningSchemaFactory() { * <ol> * <li>{@link HardeningSchemaFactory} installs an ignore-all {@link FallbackIgnoreLSResourceResolver} floor on the factory (blocking * {@code xs:import}/{@code xs:include}/{@code xs:redefine} at compile time) and rewrites the Source on every {@code newSchema(Source[])} entry point - * through {@link SAXParserHardener#hardenSource(Source)}.</li> + * through {@link HardeningSAXParserFactory#harden(Source)}.</li> * <li>{@link HardeningSchema} wraps every Validator/ValidatorHandler the inner Schema produces and re-installs the floor on each (blocking * {@code xsi:schemaLocation} at validation time), since neither the JDK nor Xerces reliably propagates it through {@code Schema}.</li> * <li>{@link HardeningValidator} rewrites the Source on every {@link Validator#validate(Source)} call.</li> * </ol> * * <p> - * The hardened reader supplied by {@link SAXParserHardener#hardenSource(Source)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a + * The hardened reader supplied by {@link HardeningSAXParserFactory#harden(Source)} already carries {@code FEATURE_SECURE_PROCESSING} and the processing limits, so a * DOCTYPE, external entity or Billion Laughs payload in the schema or instance document is bounded there rather than on this factory. The JAXP 1.5 * {@code ACCESS_EXTERNAL_*} properties are deliberately not set: the resolver floor already blocks the same fetches on every implementation, and the JDK 8 * {@code SchemaFactory} has a bug whereby those properties keep blocking even when a caller's own resolver would grant the access. The floor is a non-removable @@ -111,7 +116,7 @@ private HardeningSchemaFactory() { private static final class Wrapper extends SchemaFactory { /** - * Hardens every schema source through {@link SAXParserHardener#hardenSource(Source)}. + * Hardens every schema source through {@link HardeningSAXParserFactory#harden(Source)}. * * @param schemas the schema sources to harden; must not be {@code null}. * @return a new array of hardened sources. @@ -123,7 +128,7 @@ private static Source[] harden(final Source[] schemas) throws SAXException { final Source[] hardened = new Source[schemas.length]; try { for (int i = 0; i < schemas.length; i++) { - hardened[i] = SAXParserHardener.hardenSource(schemas[i]); + hardened[i] = HardeningSAXParserFactory.harden(schemas[i]); } } catch (final TransformerConfigurationException e) { throw new SAXException("Failed to harden schema source", e); diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformer.java b/src/main/java/org/apache/commons/xml/HardeningTransformer.java index c0ab998..39669d6 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformer.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformer.java @@ -32,7 +32,7 @@ /** * {@link Transformer} wrapper that rewrites the Source on every {@link Transformer#transform(Source, Result)} call through - * {@link SAXParserHardener#hardenSource(Source)} before delegating, and keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} calls a + * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} calls a * caller does not resolve return empty rather than being fetched. * <p> * The floor is installed on the delegate transformer at construction, seeded with the factory's compile-time resolver; {@link #setURIResolver(URIResolver)} @@ -137,7 +137,7 @@ public void setURIResolver(final URIResolver resolver) { @Override public void transform(final Source xmlSource, final Result outputTarget) throws TransformerException { try { - delegate.transform(SAXParserHardener.hardenSource(xmlSource), outputTarget); + delegate.transform(HardeningSAXParserFactory.harden(xmlSource), outputTarget); } catch (final TransformerConfigurationException e) { throw new TransformerException(e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java index 1a73f58..172c988 100644 --- a/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningTransformerFactory.java @@ -56,6 +56,43 @@ */ public final class HardeningTransformerFactory { + /** + * Capability-driven hardening for any {@link TransformerFactory} on the classpath. + * + * <p>Rather than branching on the implementation class, this method probes what the factory supports and adapts:</p> + * <ul> + * <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(TransformerFactory)} for the + * channels the standard JAXP knobs cannot close (reflection-based extension functions, the collection finder, the internal SAX parser). It is then + * wrapped like every other implementation to install the {@link FallbackIgnoreURIResolver} floor; the only + * difference is the empty-{@link Source} shape the floor returns, {@code EmptySource} for Saxon rather than the default empty DOM document.</li> + * <li><strong>FSP</strong> ({@link XMLConstants#FEATURE_SECURE_PROCESSING}): required. On XSLTC it enables the runtime evaluator limits; on Xalan it disables + * reflection-based extension functions.</li> + * <li><strong>{@link FallbackIgnoreURIResolver} floor</strong>: required. An ignore-all {@link URIResolver} floor, installed by + * the nested wrapper and carried onto every produced {@link Transformer}, resolves {@code xsl:import}/{@code xsl:include} at compile + * time and {@code document()} at runtime to an empty document, the one channel both XSLTC and Xalan route through. A caller-set {@link URIResolver} is + * routed through the floor rather than replacing it, so a caller can opt a specific URI in but cannot reopen the fetch.</li> + * <li><strong>The nested wrapper</strong>: 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 org.apache.commons.xml}-hardened reader instead.</li> + * </ul> + * + * @param factory the factory to harden; never {@code null}. + * @return a hardened factory. + */ + static TransformerFactory harden(final TransformerFactory factory) { + // Required: enables secure processing (XSLTC runtime limits; Xalan's extension-function block). + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + if (SaxonProvider.isSaxon(factory.getClass())) { + // Saxon keeps its vendor Configuration for the channels JAXP cannot close, + // then goes through the same wrapper as every other implementation for the URIResolver floor; + // EmptySource is the empty-source shape Saxon's consumers expect. + return new Wrapper((SAXTransformerFactory) SaxonProvider.configure(factory), SaxonProvider.emptySourceSupplier()); + } + // Required: source/stylesheet parsing provisions its own SAX reader otherwise; the wrapper routes every Source through a hardened one and installs the + // ignore-all URIResolver floor (blocking xsl:import/include at compile time and document() at runtime) that a caller-set resolver cannot remove. + return new Wrapper((SAXTransformerFactory) factory); + } + /** * Returns a new, hardened {@link TransformerFactory}. * <p> @@ -80,28 +117,15 @@ public final class HardeningTransformerFactory { * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. */ public static TransformerFactory newInstance() { - return TransformerHardener.harden(TransformerFactory.newInstance()); + return harden(TransformerFactory.newInstance()); } - /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. - * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. - */ - static TransformerFactory wrap(final SAXTransformerFactory delegate) { - return new Wrapper(delegate); - } - - /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. - * - * @param delegate the delegate to wrap; must not be {@code null}. - * @param emptySource supplies the empty document a denied fetch resolves to; may be {@code null} for the default empty DOM document. - * @return The hardened factory. - */ - static TransformerFactory wrap(final SAXTransformerFactory delegate, final Supplier<Source> emptySource) { - return new Wrapper(delegate, emptySource); + private static void setFeature(final TransformerFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final Exception e) { + throw HardeningException.settingFailed("feature", feature, factory, e); + } } private HardeningTransformerFactory() { @@ -109,7 +133,7 @@ private HardeningTransformerFactory() { } /** - * {@link javax.xml.transform.TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link SAXParserHardener#hardenSource(Source)} before + * {@link TransformerFactory} wrapper that rewrites every Source-taking entry point through {@link HardeningSAXParserFactory#harden(Source)} before * delegating. * * <p>Used by providers whose underlying TrAX implementation pulls a new {@code SAXParserFactory.newInstance()} for any Source that is not already a @@ -144,10 +168,10 @@ private static final class Wrapper extends SAXTransformerFactory { /** * Parses a reader-less source into a DOM through a hardened, namespace-aware {@link javax.xml.parsers.DocumentBuilder} and returns a {@link DOMSource} * carrying its system id, so the consumer walks the tree instead of provisioning its own reader. Any other source is left to - * {@link SAXParserHardener#hardenSource(Source)}. + * {@link HardeningSAXParserFactory#harden(Source)}. * * @param source The source to scan for an associated stylesheet. - * @return A {@link DOMSource} for a reader-less source, otherwise the result of {@link SAXParserHardener#hardenSource(Source)}. + * @return A {@link DOMSource} for a reader-less source, otherwise the result of {@link HardeningSAXParserFactory#harden(Source)}. * @throws TransformerConfigurationException if the source cannot be parsed. * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service * configuration error} or if the implementation is not available or cannot be instantiated. @@ -158,7 +182,7 @@ private static Source hardenSourceToDom(final Source source) throws TransformerC final InputSource inputSource = SAXSource.sourceToInputSource(source); if (inputSource != null) { try { - final DocumentBuilderFactory factory = DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); factory.setNamespaceAware(true); final Document document = factory.newDocumentBuilder().parse(inputSource); return new DOMSource(document, inputSource.getSystemId()); @@ -167,7 +191,7 @@ private static Source hardenSourceToDom(final Source source) throws TransformerC } } } - return SAXParserHardener.hardenSource(source); + return HardeningSAXParserFactory.harden(source); } /** @@ -229,7 +253,7 @@ private Wrapper(final SAXTransformerFactory delegate, final Supplier<Source> emp public Source getAssociatedStylesheet(final Source source, final String media, final String title, final String charset) throws TransformerConfigurationException { // Xalan's getAssociatedStylesheet drops a SAXSource's reader and self-provisions its own to scan for xml-stylesheet PIs (XALANJ-2849). - final Source hardened = isXalan(delegate) ? hardenSourceToDom(source) : SAXParserHardener.hardenSource(source); + final Source hardened = isXalan(delegate) ? hardenSourceToDom(source) : HardeningSAXParserFactory.harden(source); return delegate.getAssociatedStylesheet(hardened, media, title, charset); } @@ -265,7 +289,7 @@ private TransformerHandler hardenHandler(final TransformerHandler handler) { */ @Override public Templates newTemplates(final Source source) throws TransformerConfigurationException { - final Templates templates = delegate.newTemplates(SAXParserHardener.hardenSource(source)); + final Templates templates = delegate.newTemplates(HardeningSAXParserFactory.harden(source)); return templates == null ? null : new HardeningTemplates(templates, getURIResolver(), emptySource); } @@ -290,7 +314,7 @@ public Transformer newTransformer() throws TransformerConfigurationException { */ @Override public Transformer newTransformer(final Source source) throws TransformerConfigurationException { - final Transformer transformer = delegate.newTransformer(SAXParserHardener.hardenSource(source)); + final Transformer transformer = delegate.newTransformer(HardeningSAXParserFactory.harden(source)); return transformer == null ? null : new HardeningTransformer(transformer, getURIResolver(), emptySource); } @@ -307,7 +331,7 @@ public TransformerHandler newTransformerHandler() throws TransformerConfiguratio */ @Override public TransformerHandler newTransformerHandler(final Source source) throws TransformerConfigurationException { - return hardenHandler(delegate.newTransformerHandler(SAXParserHardener.hardenSource(source))); + return hardenHandler(delegate.newTransformerHandler(HardeningSAXParserFactory.harden(source))); } @Override diff --git a/src/main/java/org/apache/commons/xml/HardeningValidator.java b/src/main/java/org/apache/commons/xml/HardeningValidator.java index 6c61c7b..92d06fc 100644 --- a/src/main/java/org/apache/commons/xml/HardeningValidator.java +++ b/src/main/java/org/apache/commons/xml/HardeningValidator.java @@ -34,7 +34,7 @@ /** * {@link Validator} wrapper that rewrites the Source on every {@link Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call through - * {@link SAXParserHardener#hardenSource(Source)} before delegating, and keeps an ignore-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is not resolved at + * {@link HardeningSAXParserFactory#harden(Source)} before delegating, and keeps an ignore-all {@link LSResourceResolver} floor so {@code xsi:schemaLocation} is not resolved at * validation time. {@link #reset()} re-establishes the bare ignore-all floor, matching the just-constructed state. */ final class HardeningValidator extends Validator { @@ -113,7 +113,7 @@ public void setResourceResolver(final LSResourceResolver resourceResolver) { @Override public void validate(final Source source, final Result result) throws SAXException, IOException { try { - delegate.validate(SAXParserHardener.hardenSource(source), result); + delegate.validate(HardeningSAXParserFactory.harden(source), result); } catch (final TransformerConfigurationException e) { throw new SAXException("Failed to harden source for validation", e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java index a8ad74b..1a8533e 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLFilter.java @@ -70,7 +70,7 @@ public void parse(final InputSource input) throws SAXException, IOException { } if (getParent() == null) { try { - setParent(SAXParserHardener.newHardenedReader()); + setParent(HardeningSAXParserFactory.newHardenedReader()); } catch (final TransformerException e) { throw new SAXException(e); } diff --git a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java index 5e9bc0e..37ed393 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXMLInputFactory.java @@ -44,6 +44,28 @@ */ public final class HardeningXMLInputFactory { + /** Woodstox property: resolver consulted for the external DTD subset. */ + static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver"; + /** Woodstox property: resolver consulted for declared external general entities. */ + static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver"; + /** Woodstox property: resolver consulted for undeclared entity references. */ + static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver"; + + /** + * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath. + * + * <p>One recipe covers both the JDK Zephyr and Woodstox: the wrapper installs a non-removable {@link FallbackIgnoreXMLResolver} floor on + * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see the wrapper's + * Javadoc for the per-implementation hook routing.</p> + * + * @param factory the factory to harden; never {@code null}. + * @return a hardened factory. + */ + static XMLInputFactory harden(final XMLInputFactory factory) { + // The wrapper installs the non-removable ignore-all resolver floor that resolves every external DTD and entity to empty content. + return new Wrapper(factory); + } + /** * Returns a new, hardened {@link XMLInputFactory}. * <p> @@ -55,17 +77,7 @@ public final class HardeningXMLInputFactory { * @throws FactoryConfigurationError Thrown if an instance of this factory cannot be loaded. */ public static XMLInputFactory newInstance() { - return StaxHardener.harden(XMLInputFactory.newInstance()); - } - - /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. - * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. - */ - static XMLInputFactory wrap(final XMLInputFactory delegate) { - return new Wrapper(delegate); + return harden(XMLInputFactory.newInstance()); } private HardeningXMLInputFactory() { @@ -77,7 +89,7 @@ private HardeningXMLInputFactory() { * non-removable by the caller. * * <p>The constructor installs the floor through {@code setXMLResolver}, which every implementation routes external resolution through (Woodstox fans it out to - * both its DTD-subset and entity resolvers). Woodstox keeps one hook outside that fan-out, {@value StaxHardener#WSTX_UNDECLARED_ENTITY_RESOLVER}, which is + * both its DTD-subset and entity resolvers). Woodstox keeps one hook outside that fan-out, {@value HardeningXMLInputFactory#WSTX_UNDECLARED_ENTITY_RESOLVER}, which is * deliberately left empty: emptying the external subset leaves any entity it declared undeclared, and Woodstox then rejects the reference. The rejection is * implementation-prescribed and keeps the resource just as unfetched as the empty resolution the other implementations produce; a caller who wants those * references resolved can still set the property, and their resolver lands behind a floor like on every other resolver hook.</p> @@ -96,9 +108,9 @@ private static final class Wrapper extends XMLInputFactory { private static boolean isResolverProperty(final String name) { return XMLInputFactory.RESOLVER.equals(name) - || StaxHardener.WSTX_DTD_RESOLVER.equals(name) - || StaxHardener.WSTX_ENTITY_RESOLVER.equals(name) - || StaxHardener.WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name); + || WSTX_DTD_RESOLVER.equals(name) + || WSTX_ENTITY_RESOLVER.equals(name) + || WSTX_UNDECLARED_ENTITY_RESOLVER.equals(name); } diff --git a/src/main/java/org/apache/commons/xml/HardeningXPath.java b/src/main/java/org/apache/commons/xml/HardeningXPath.java index 4825629..abac0b7 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPath.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPath.java @@ -66,7 +66,7 @@ final class HardeningXPath implements XPath { static Document parse(final InputSource source) throws XPathExpressionException { Objects.requireNonNull(source, "source"); try { - final DocumentBuilderFactory factory = DocumentBuilderHardener.harden(DocumentBuilderFactory.newInstance()); + final DocumentBuilderFactory factory = HardeningDocumentBuilderFactory.harden(DocumentBuilderFactory.newInstance()); factory.setNamespaceAware(true); return factory.newDocumentBuilder().parse(source); } catch (final ParserConfigurationException | SAXException | IOException e) { diff --git a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java index 460efad..eb33faf 100644 --- a/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java +++ b/src/main/java/org/apache/commons/xml/HardeningXPathFactory.java @@ -19,6 +19,7 @@ import java.util.Objects; +import javax.xml.XMLConstants; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathFactory; import javax.xml.xpath.XPathFactoryConfigurationException; @@ -52,19 +53,84 @@ public final class HardeningXPathFactory { * @throws RuntimeException Thrown if there is a failure in creating an {@link XPathFactory} for the default object model. */ public static XPathFactory newInstance() { - return XPathHardener.harden(XPathFactory.newInstance()); + return harden(XPathFactory.newInstance()); } /** - * Wraps a prepared delegate in the hardening wrapper; called by the hardener once the required settings are applied. + * Capability-driven hardening for any {@link XPathFactory} on the classpath. * - * @param delegate the delegate to wrap; must not be {@code null}. - * @return The hardened factory. + * <p>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, this method probes what the factory supports and adapts:</p> + * <ul> + * <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so any public + * subclass routes to the same recipe as the registered factory. 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 HardeningTransformerFactory#harden(javax.xml.transform.TransformerFactory)}, kept as a + * documented package-prefix exception because the required hardening surface is reachable only through a vendor API.</li> + * <li><strong>FODP</strong> ({@code jdk.xml.overrideDefaultParser}, set to {@code false}): best-effort. On the stock JDK it pins the internal parser lookup to + * the bundled SAX parser, blocking a system property swap to a third-party parser (defense-in-depth); Xalan rejects the feature and is left unchanged.</li> + * <li><strong>FSP</strong> ({@link javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING}): required. It is the only knob both the stock JDK and Xalan XPath + * engines expose, and switches on their secure-processing limits. {@link XPathFactory} has no attribute API for finer control.</li> + * <li><strong>The nested wrapper</strong>: required. FSP governs only the engine, not the parser it provisions internally for the + * {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points; the wrapper performs that document build with a hardened parser instead, so + * the engine never parses.</li> + * </ul> + * + * @param factory The factory to harden. + * @return A new hardened factory or the original factory, hardened, if it is a known Saxon factory. + * @throws HardeningException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature. */ - static XPathFactory wrap(final XPathFactory delegate) { - return new Wrapper(delegate); + static XPathFactory harden(final XPathFactory factory) { + if (SaxonProvider.isSaxon(factory.getClass())) { + // Saxon: only a locked-down Configuration can close its URI-fetching functions and extension-function surface. + return SaxonProvider.configure(factory); + } + // Best-effort: the stock JDK pins its bundled SAX parser (defense-in-depth); Xalan rejects the feature. + setOptionalFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); + // Required: enables the engine's secure-processing limits; XPathFactory has no attribute API for finer control. + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); + // Required: FSP does not reach the parser the engine provisions for InputSource-taking evaluate calls; the wrapper parses those itself. + return new Wrapper(factory); } + /** + * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. + * + * @param factory The factory to harden. + * @param feature The feature to set. + * @param value The value to set. + * @throws HardeningException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature or if {@code feature} is + * {@code null}. + */ + private static void setFeature(final XPathFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final XPathFactoryConfigurationException e) { + throw HardeningException.settingFailed("feature", feature, factory, e); + } + } + + /** + * Sets an optional feature on the given factory, ignoring it if the implementation does not recognize it. + * + * @param factory The factory to harden. + * @param feature The feature to set. + * @param value The value to set. + */ + private static void setOptionalFeature(final XPathFactory factory, final String feature, final boolean value) { + try { + factory.setFeature(feature, value); + } catch (final XPathFactoryConfigurationException e) { + // Ignored: the implementation does not recognize this optional feature. + } + } + + /** + * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX parser; defense-in-depth against a system property swap to a third-party parser. + */ + private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; + private HardeningXPathFactory() { // static only } diff --git a/src/main/java/org/apache/commons/xml/SAXParserHardener.java b/src/main/java/org/apache/commons/xml/SAXParserHardener.java deleted file mode 100644 index f1c7760..0000000 --- a/src/main/java/org/apache/commons/xml/SAXParserHardener.java +++ /dev/null @@ -1,180 +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.XMLConstants; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParserFactory; -import javax.xml.transform.Source; -import javax.xml.transform.TransformerConfigurationException; -import javax.xml.transform.sax.SAXSource; -import javax.xml.transform.stream.StreamSource; - -import org.xml.sax.EntityResolver; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; -import org.xml.sax.SAXNotRecognizedException; -import org.xml.sax.SAXNotSupportedException; -import org.xml.sax.XMLReader; - -/** - * Capability-driven hardening for any {@link SAXParserFactory} on the classpath. - * - * <p>Rather than branching on the implementation class, {@link #harden(SAXParserFactory)} probes what the parser supports and adapts. Because - * {@link SAXParserFactory} exposes only a feature API and no property API, the per-parse configuration runs on each {@link XMLReader} the factory produces, - * funnelled through {@link HardeningSAXParserFactory} into {@link #hardenReader(XMLReader)}:</p> - * <ul> - * <li><strong>Android</strong> (Harmony / Expat): {@link XMLConstants#FEATURE_SECURE_PROCESSING FSP} and the JAXP 1.5 {@code ACCESS_EXTERNAL_*} properties - * are not recognized, and libexpat enforces its own Billion Laughs check, so neither is applied. Two fixups are still needed: an ignore-all resolver - * (Expat ignores external fetches silently when no resolver is set; the floor keeps that behavior non-bypassable, resolving anything unresolved to - * empty), and a {@link HardeningExpatXMLReader} so the unsupported {@code namespace-prefixes} feature is rejected at - * configuration time rather than mid-parse.</li> - * <li><strong>FSP</strong>: required on every other reader. It switches on the implementation's built-in security manager, which is what carries the - * processing limits.</li> - * <li><strong>Ignore-all resolver floor</strong>: every reader is wrapped in a {@link HardeningXMLReader} that keeps an ignore-all {@link EntityResolver} floor. - * That floor blocks external DTD, entity, schema and {@code xi:include} fetches in one place: the stock JDK's XInclude processor ignores - * {@code ACCESS_EXTERNAL_*} and consults the {@link EntityResolver} instead, so no {@code ACCESS_EXTERNAL_*} properties are needed here. A caller can - * chain its own resolver onto the floor to allow-list resources, but cannot remove it.</li> - * </ul> - */ -final class SAXParserHardener { - - /** - * {@link HardeningXMLReader} for Android's {@code org.apache.harmony.xml.ExpatReader} that additionally surfaces its {@code namespace-prefixes} limitation at - * configuration time. - * - * <p>ExpatReader does not actually support the {@code namespace-prefixes} feature: enabling it is accepted by {@code setFeature} but fails later, during - * {@code parse}, with a {@link SAXNotSupportedException}. Reporting the rejection eagerly from {@link #setFeature(String, boolean)} lets consumers that probe - * the feature, such as Xalan's identity transformer, catch the exception and fall back instead of failing the whole parse.</p> - */ - static final class HardeningExpatXMLReader extends HardeningXMLReader { - - private static final String NAMESPACE_PREFIXES_FEATURE = "http://xml.org/sax/features/namespace-prefixes"; - - HardeningExpatXMLReader(final XMLReader delegate) { - super(delegate); - } - - @Override - public void setFeature(final String name, final boolean value) throws SAXNotRecognizedException, SAXNotSupportedException { - if (value && NAMESPACE_PREFIXES_FEATURE.equals(name)) { - throw new SAXNotSupportedException("ExpatReader does not support enabling the '" + NAMESPACE_PREFIXES_FEATURE + "' feature"); - } - super.setFeature(name, value); - } - } - - /** Class name of Android's Harmony-based {@link SAXParserFactory}, backed by the native Expat parser. */ - private static final String ANDROID_SAX_PARSER_FACTORY = "org.apache.harmony.xml.parsers.SAXParserFactoryImpl"; - - /** Class name of Android's Expat-backed {@link XMLReader}. */ - private static final String ANDROID_EXPAT_READER = "org.apache.harmony.xml.ExpatReader"; - - static SAXParserFactory harden(final SAXParserFactory factory) { - // Required: enables the implementation's security manager, which carries the limits. Android's Expat rejects FSP, so it is skipped there. - if (!ANDROID_SAX_PARSER_FACTORY.equals(factory.getClass().getName())) { - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - } - // The per-parse hardening (limits, entity blocking, Android fixups) lives in hardenReader() because SAXParserFactory has no property API. - return HardeningSAXParserFactory.wrap(factory); - } - - /** - * Hardens an existing {@link XMLReader}. - * - * @param reader The reader to harden; never {@code null}. - * @return A hardened reader. - * @throws IllegalStateException if a required hardening setting cannot be applied to the underlying implementation. - */ - static XMLReader hardenReader(final XMLReader reader) { - if (reader instanceof HardeningXMLReader) { - // Already hardened (for example, a reader from a hardened factory passed back through hardenReader); the floor is already in place. - return reader; - } - if (ANDROID_EXPAT_READER.equals(reader.getClass().getName())) { - // Expat ignores external fetches when no resolver is set; the ignore-all floor keeps that behavior non-bypassable (routing a caller-set resolver, - // including SAXParser.parse's handler, through it and resolving anything unresolved to empty) and, via HardeningExpatXMLReader, rejects the - // unsupported namespace-prefixes feature eagerly rather than mid-parse. - return new HardeningExpatXMLReader(reader); - } - // Required: enables the JDK XMLSecurityManager / Xerces SecurityManager limits. - setFeature(reader, XMLConstants.FEATURE_SECURE_PROCESSING, true); - // Required: HardeningXMLReader installs an ignore-all EntityResolver floor on the reader. - // That floor blocks external DTD, entity, schema and xi:include fetches in one place: no ACCESS_EXTERNAL_* properties are needed here. - // Callers can chain their resolvers, but not override the floor. - return new HardeningXMLReader(reader); - } - - /** - * Rewrites a {@link Source} so that any SAX parsing it triggers runs through a hardened {@link XMLReader}. - * <p> - * Only a {@link StreamSource} or a {@link SAXSource} without a reader is enriched with a hardened, namespace-aware reader; other source kinds are returned - * as-is. Used by the TrAX and schema wrappers to route every source they parse through the SAX hardening path. - * </p> - * - * @param source the source to harden; never {@code null}. - * @return a hardened source. - * @throws TransformerConfigurationException if a hardened reader cannot be obtained. - * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service - * configuration error} or if the implementation is not available or cannot be instantiated. - */ - static Source hardenSource(final Source source) throws TransformerConfigurationException { - if (source instanceof StreamSource || source instanceof SAXSource && ((SAXSource) source).getXMLReader() == null) { - final InputSource inputSource = SAXSource.sourceToInputSource(source); - return inputSource == null ? source : new SAXSource(newHardenedReader(), inputSource); - } - return source; - } - - /** - * Creates a new hardened, namespace-aware {@link XMLReader} for the TrAX wrappers to parse sources with. - * - * @return a hardened reader. - * @throws TransformerConfigurationException if a hardened reader cannot be obtained. - * @throws FactoryConfigurationError Thrown from a factory in case of a {@link java.util.ServiceConfigurationError service - * configuration error} or if the implementation is not available or cannot be instantiated. - */ - static XMLReader newHardenedReader() throws TransformerConfigurationException { - try { - final SAXParserFactory factory = harden(SAXParserFactory.newInstance()); - factory.setNamespaceAware(true); - return factory.newSAXParser().getXMLReader(); - } catch (final ParserConfigurationException | SAXException e) { - throw new TransformerConfigurationException("Failed to obtain a hardened XMLReader for source parsing", e); - } - } - - private static void setFeature(final SAXParserFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final Exception e) { - throw HardeningException.settingFailed("feature", feature, factory, e); - } - } - - private static void setFeature(final XMLReader reader, final String feature, final boolean value) { - try { - reader.setFeature(feature, value); - } catch (final Exception e) { - throw HardeningException.settingFailed("feature", feature, reader, e); - } - } - - private SAXParserHardener() { - } -} diff --git a/src/main/java/org/apache/commons/xml/SaxonProvider.java b/src/main/java/org/apache/commons/xml/SaxonProvider.java index 9453796..935b422 100644 --- a/src/main/java/org/apache/commons/xml/SaxonProvider.java +++ b/src/main/java/org/apache/commons/xml/SaxonProvider.java @@ -52,7 +52,7 @@ final class SaxonProvider { * * <ol> * <li><b>SAX layer.</b> {@link #makeParser} hands every {@link XMLReader} Saxon would otherwise use through - * {@link SAXParserHardener#hardenReader(XMLReader)}, which routes it to the matching bundled hardening recipe. External DTDs, entities and XInclude + * {@link HardeningSAXParserFactory#harden(XMLReader)}, which routes it to the matching bundled hardening recipe. External DTDs, entities and XInclude * resolve to empty content at parse time.</li> * <li><b>Collection layer.</b> {@code fn:collection} bypasses the resource resolver and fetches directly, so an empty {@link CollectionFinder} supplies its * ignore outcome instead.</li> @@ -87,7 +87,7 @@ private HardenedConfiguration() { @Override public XMLReader makeParser(final String className) throws TransformerFactoryConfigurationError { try { - return SAXParserHardener.hardenReader(super.makeParser(className)); + return HardeningSAXParserFactory.harden(super.makeParser(className)); } catch (final HardeningException e) { throw new TransformerFactoryConfigurationError(e); } @@ -103,7 +103,7 @@ public XMLReader makeParser(final String className) throws TransformerFactoryCon private static final class SaxonProviderConfigurer { private static TransformerFactory configure(final TransformerFactory factory) { - // The URIResolver floor is installed by the HardeningTransformerFactory wrapper that TransformerHardener puts around this factory. + // The URIResolver floor is installed by the HardeningTransformerFactory wrapper that HardeningTransformerFactory.harden puts around this factory. ((SaxonTransformerFactory) factory).setConfiguration(new HardenedConfiguration()); return factory; } diff --git a/src/main/java/org/apache/commons/xml/SchemaHardener.java b/src/main/java/org/apache/commons/xml/SchemaHardener.java deleted file mode 100644 index ac355a8..0000000 --- a/src/main/java/org/apache/commons/xml/SchemaHardener.java +++ /dev/null @@ -1,53 +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.validation.SchemaFactory; - -/** - * Hardening for any {@link SchemaFactory} on the classpath. - * - * <p>Unlike the other hardeners there is no per-implementation branching and no feature or limit configuration on the factory itself: schema compilation and - * validation reach external resources only through the resolver hook, so wrapping the factory with a non-removable ignore-all resolver floor is enough on every - * implementation. The reader used to parse schema and instance documents is hardened separately, through - * {@link SAXParserHardener#hardenSource(javax.xml.transform.Source)}.</p> - */ -final class SchemaHardener { - - /** - * Hardens an existing {@link SchemaFactory}. - * - * <p>Beyond the three universal guarantees (no external DTD fetch, no external entity resolution, bounded internal entity expansion):</p> - * <ul> - * <li>{@code xs:import}, {@code xs:include} and {@code xs:redefine} schemaLocation URIs are not resolved during schema compilation, and</li> - * <li>{@code xsi:schemaLocation} / {@code xsi:noNamespaceSchemaLocation} hints in instance documents are not resolved during validation.</li> - * </ul> - * - * <p>The same guarantees apply to {@link javax.xml.validation.Validator} and {@link javax.xml.validation.ValidatorHandler} instances produced from the - * resulting {@link javax.xml.validation.Schema}.</p> - * - * @param factory the factory to harden; never {@code null}. - * @return a hardened factory. - */ - static SchemaFactory harden(final SchemaFactory factory) { - return HardeningSchemaFactory.wrap(factory); - } - - private SchemaHardener() { - } -} diff --git a/src/main/java/org/apache/commons/xml/StaxHardener.java b/src/main/java/org/apache/commons/xml/StaxHardener.java deleted file mode 100644 index db4a7d0..0000000 --- a/src/main/java/org/apache/commons/xml/StaxHardener.java +++ /dev/null @@ -1,47 +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; - -/** - * Capability-driven hardening for any {@link XMLInputFactory} (StAX) on the classpath. - * - * <p>One recipe covers both the JDK Zephyr and Woodstox: {@link HardeningXMLInputFactory} installs a non-removable {@link FallbackIgnoreXMLResolver} floor on - * every entity-resolution hook, leaving the standard {@code SUPPORT_DTD} / {@code IS_SUPPORTING_EXTERNAL_ENTITIES} defaults untouched; see that wrapper's - * Javadoc for the per-implementation hook routing.</p> - */ -final class StaxHardener { - - /** Woodstox property: resolver consulted for the external DTD subset. */ - static final String WSTX_DTD_RESOLVER = "com.ctc.wstx.dtdResolver"; - - /** Woodstox property: resolver consulted for declared external general entities. */ - static final String WSTX_ENTITY_RESOLVER = "com.ctc.wstx.entityResolver"; - - /** Woodstox property: resolver consulted for undeclared entity references. */ - static final String WSTX_UNDECLARED_ENTITY_RESOLVER = "com.ctc.wstx.undeclaredEntityResolver"; - - static XMLInputFactory harden(final XMLInputFactory factory) { - // HardeningXMLInputFactory installs the non-removable ignore-all resolver floor that resolves every external DTD and entity to empty content. - return HardeningXMLInputFactory.wrap(factory); - } - - private StaxHardener() { - } -} diff --git a/src/main/java/org/apache/commons/xml/TransformerHardener.java b/src/main/java/org/apache/commons/xml/TransformerHardener.java deleted file mode 100644 index e7d6c0e..0000000 --- a/src/main/java/org/apache/commons/xml/TransformerHardener.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 javax.xml.XMLConstants; -import javax.xml.transform.Source; -import javax.xml.transform.Transformer; -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. - * - * <p>Rather than branching on the implementation class, {@link #harden(TransformerFactory)} probes what the factory supports and adapts:</p> - * <ul> - * <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(TransformerFactory)} for the - * channels the standard JAXP knobs cannot close (reflection-based extension functions, the collection finder, the internal SAX parser). It is then - * wrapped in {@link HardeningTransformerFactory} like every other implementation to install the {@link FallbackIgnoreURIResolver} floor; the only - * difference is the empty-{@link Source} shape the floor returns, {@code EmptySource} for Saxon rather than the default empty DOM document.</li> - * <li><strong>FSP</strong> ({@link XMLConstants#FEATURE_SECURE_PROCESSING}): required. On XSLTC it enables the runtime evaluator limits; on Xalan it disables - * reflection-based extension functions.</li> - * <li><strong>{@link FallbackIgnoreURIResolver} floor</strong>: required. An ignore-all {@link URIResolver} floor, installed by - * {@link HardeningTransformerFactory} and carried onto every produced {@link Transformer}, resolves {@code xsl:import}/{@code xsl:include} at compile - * time and {@code document()} at runtime to an empty document, the one channel both XSLTC and Xalan route through. A caller-set {@link URIResolver} is - * routed through the floor rather than replacing it, so a caller can opt a specific URI in but cannot reopen the fetch.</li> - * <li><strong>{@link HardeningTransformerFactory}</strong>: 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 org.apache.commons.xml}-hardened reader instead.</li> - * </ul> - */ -final class TransformerHardener { - - static TransformerFactory harden(final TransformerFactory factory) { - // Required: enables secure processing (XSLTC runtime limits; Xalan's extension-function block). - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - if (SaxonProvider.isSaxon(factory.getClass())) { - // Saxon keeps its vendor Configuration for the channels JAXP cannot close, - // then goes through the same wrapper as every other implementation for the URIResolver floor; - // EmptySource is the empty-source shape Saxon's consumers expect. - return HardeningTransformerFactory.wrap((SAXTransformerFactory) SaxonProvider.configure(factory), SaxonProvider.emptySourceSupplier()); - } - // Required: source/stylesheet parsing provisions its own SAX reader otherwise; the wrapper routes every Source through a hardened one and installs the - // ignore-all URIResolver floor (blocking xsl:import/include at compile time and document() at runtime) that a caller-set resolver cannot remove. - return HardeningTransformerFactory.wrap((SAXTransformerFactory) factory); - } - - private static void setFeature(final TransformerFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final Exception e) { - throw HardeningException.settingFailed("feature", feature, factory, e); - } - } - - private TransformerHardener() { - } -} diff --git a/src/main/java/org/apache/commons/xml/XPathHardener.java b/src/main/java/org/apache/commons/xml/XPathHardener.java deleted file mode 100644 index fccbfd5..0000000 --- a/src/main/java/org/apache/commons/xml/XPathHardener.java +++ /dev/null @@ -1,106 +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.XMLConstants; -import javax.xml.xpath.XPathFactory; -import javax.xml.xpath.XPathFactoryConfigurationException; - -/** - * Capability-driven hardening for any {@link XPathFactory} on the classpath. - * - * <p>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:</p> - * <ul> - * <li><strong>Saxon</strong> ({@code net.sf.saxon}): recognized by package prefix and handed to {@link SaxonProvider#configure(XPathFactory)}, so any public - * subclass routes to the same recipe as the registered factory. 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 package-prefix exception because the required - * hardening surface is reachable only through a vendor API.</li> - * <li><strong>FODP</strong> ({@code jdk.xml.overrideDefaultParser}, set to {@code false}): best-effort. On the stock JDK it pins the internal parser lookup to - * the bundled SAX parser, blocking a system property swap to a third-party parser (defense-in-depth); Xalan rejects the feature and is left unchanged.</li> - * <li><strong>FSP</strong> ({@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.</li> - * <li><strong>{@link HardeningXPathFactory}</strong>: required. FSP governs only the engine, not the parser it provisions internally for the - * {@link org.xml.sax.InputSource}-taking {@code evaluate} entry points; the wrapper performs that document build with a hardened parser instead, so - * the engine never parses.</li> - * </ul> - */ -final class XPathHardener { - - /** - * {@code jdk.xml.overrideDefaultParser}: pin to the JDK's bundled SAX parser; defense-in-depth against a system property swap to a third-party parser. - */ - private static final String FEATURE_OVERRIDE_DEFAULT_PARSER = "jdk.xml.overrideDefaultParser"; - - /** - * Hardens the given factory, returning a hardened wrapper if necessary. - * - * @param factory The factory to harden. - * @return A new hardened factory or the original factory, hardened, if it is a known Saxon factory. - * @throws HardeningException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature. - */ - static XPathFactory harden(final XPathFactory factory) { - if (SaxonProvider.isSaxon(factory.getClass())) { - // Saxon: only a locked-down Configuration can close its URI-fetching functions and extension-function surface. - return SaxonProvider.configure(factory); - } - // Best-effort: the stock JDK pins its bundled SAX parser (defense-in-depth); Xalan rejects the feature. - setOptionalFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false); - // Required: enables the engine's secure-processing limits; XPathFactory has no attribute API for finer control. - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - // Required: FSP does not reach the parser the engine provisions for InputSource-taking evaluate calls; the wrapper parses those itself. - return HardeningXPathFactory.wrap(factory); - } - - /** - * Sets a feature on the given factory, throwing a {@link HardeningException} if the implementation does not recognize it. - * - * @param factory The factory to harden. - * @param feature The feature to set. - * @param value The value to set. - * @throws HardeningException Thrown if this {@link XPathFactory} or the {@code XPath}s it creates cannot support this feature or if {@code feature} is - * {@code null}. - */ - private static void setFeature(final XPathFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final XPathFactoryConfigurationException e) { - throw HardeningException.settingFailed("feature", feature, factory, e); - } - } - - /** - * Sets an optional feature on the given factory, ignoring it if the implementation does not recognize it. - * - * @param factory The factory to harden. - * @param feature The feature to set. - * @param value The value to set. - */ - private static void setOptionalFeature(final XPathFactory factory, final String feature, final boolean value) { - try { - factory.setFeature(feature, value); - } catch (final XPathFactoryConfigurationException e) { - // Ignored: the implementation does not recognize this optional feature. - } - } - - private XPathHardener() { - } -} diff --git a/src/test/java/org/apache/commons/xml/AttackTestSupport.java b/src/test/java/org/apache/commons/xml/AttackTestSupport.java index 0e3ea63..0edeae0 100644 --- a/src/test/java/org/apache/commons/xml/AttackTestSupport.java +++ b/src/test/java/org/apache/commons/xml/AttackTestSupport.java @@ -91,7 +91,7 @@ final class AttackTestSupport { /** - * Test-only permissive counterpart of {@code SAXParserHardener.HardeningExpatXMLReader}: a pass-through Expat wrapper that rejects the + * Test-only permissive counterpart of {@code HardeningSAXParserFactory.HardeningExpatXMLReader}: a pass-through Expat wrapper that rejects the * {@code namespace-prefixes} feature eagerly (so a probing TrAX identity transformer falls back instead of failing the whole parse) but installs no ignore-all * resolver floor, so the unconfigured/positive controls stay permissive. */ @@ -691,7 +691,7 @@ static void assertValidatorValidates(final String xml) { /** * Asserts a hardened-in-place XMLReader parse of the payload throws. * - * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link SAXParserHardener#hardenReader(XMLReader)}; only a thrown exception passes.</p> + * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link HardeningSAXParserFactory#harden(XMLReader)}; only a thrown exception passes.</p> */ static void assertXmlReaderBlocks(final String payload) { assertParseFails(() -> consumeXmlReader(rawHardenedReader(), payload), "XMLReader", SAXException.class); @@ -707,7 +707,7 @@ static void assertXmlReaderBlocksOrDoesNotLeak(final String payload) { /** * Asserts a hardened-in-place XMLReader parse completes without throwing and without leaked content. * - * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link SAXParserHardener#hardenReader(XMLReader)}; use this when the hardening contract + * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link HardeningSAXParserFactory#harden(XMLReader)}; use this when the hardening contract * guarantees the parse succeeds but never resolves the external resource.</p> */ static void assertXmlReaderDoesNotLeak(final String payload) { @@ -717,7 +717,7 @@ static void assertXmlReaderDoesNotLeak(final String payload) { /** * Asserts a hardened-in-place XMLReader parse succeeds. * - * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link SAXParserHardener#hardenReader(XMLReader)}; positive control for DOCTYPE-only + * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via {@link HardeningSAXParserFactory#harden(XMLReader)}; positive control for DOCTYPE-only * payloads.</p> */ static void assertXmlReaderParses(final String payload) { @@ -926,13 +926,13 @@ private static boolean probeDomResolvesInternalEntities() { } } - /** Builds a raw {@link XMLReader} from a deliberately permissive {@link SAXParserFactory} and hardens it via {@link SAXParserHardener#hardenReader(XMLReader)}. */ + /** Builds a raw {@link XMLReader} from a deliberately permissive {@link SAXParserFactory} and hardens it via {@link HardeningSAXParserFactory#harden(XMLReader)}. */ private static XMLReader rawHardenedReader() throws Exception { final SAXParserFactory factory = SAXParserFactory.newInstance(); if (!IS_ANDROID) { factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false); } - return SAXParserHardener.hardenReader(factory.newSAXParser().getXMLReader()); + return HardeningSAXParserFactory.harden(factory.newSAXParser().getXMLReader()); } /** Opens the named test resource as a {@link StreamSource} preserving its system id, so relative includes/imports/redefines resolve normally. */ diff --git a/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java b/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java index ea21886..475cfd0 100644 --- a/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java +++ b/src/test/java/org/apache/commons/xml/SaxonAlternateFactoryTest.java @@ -87,7 +87,7 @@ private static String transform(final TransformerFactory factory) throws Transfo void hardenedBasicFactoryDoesNotLeakCollection() { assumeSaxonPresent(); try { - final String result = transform(TransformerHardener.harden(basicSaxonFactory())); + final String result = transform(HardeningTransformerFactory.harden(basicSaxonFactory())); assertFalse(result.contains(AttackTestSupport.LEAKED_MARKER), "collection() leaked through the alternate Saxon factory:\n" + result); } catch (final TransformerException blocked) { // Throwing is an acceptable outcome since it also prevents leaking the marker. diff --git a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java index f1554ba..57ea76f 100644 --- a/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java +++ b/src/test/java/org/apache/commons/xml/ShadingFootprintTest.java @@ -39,12 +39,12 @@ import org.vafer.jdependency.Clazzpath; /** - * Guards the shade footprint: the set of classes a consumer pulls in when they shade a single hardener entry point. + * Guards the shade footprint: the set of classes a consumer pulls in when they shade a single factory entry point. * * <p>Using {@code jdependency}, the same library {@code maven-shade-plugin}'s {@code minimizeJar} uses, this test computes each entry point's transitive class - * closure over the compiled {@code target/classes} and pins it to an expected set. It keeps each hardener from silently regaining a dependency on classes it - * should not need (for example a sibling resolver floor or another hardener), so schema builds only on the shared SAX path, TrAX and XPath additionally on the - * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while only the public {@link org.apache.commons.xml} entry pulls the whole + * closure over the compiled {@code target/classes} and pins it to an expected set. It keeps each entry point from silently regaining a dependency on classes it + * should not need (for example a sibling resolver floor or another factory class), so schema builds only on the shared SAX path, TrAX and XPath additionally on the + * DOM path their Xalan getAssociatedStylesheet and InputSource rewrites parse through, while the six public entry points together pull the whole * library. Update the expected sets deliberately: a change here is a change to what a downstream shade includes.</p> * * <p>The test reads the compiled {@code .class} files from the code-source location, which only exists on a regular JVM: a native image carries no bytecode (and @@ -56,8 +56,7 @@ class ShadingFootprintTest { private static final String PKG = "org.apache.commons.xml."; // @formatter:off - private static final Set<String> DOCUMENT_BUILDER_HARDENER = set( - "DocumentBuilderHardener", + private static final Set<String> DOCUMENT_BUILDER_FACTORY = set( "FallbackIgnoreEntityResolver2", "HardeningDocumentBuilder", "HardeningDocumentBuilderFactory", @@ -67,36 +66,33 @@ class ShadingFootprintTest { // @formatter:on // @formatter:off - private static final Set<String> SAX_PARSER_HARDENER = set( + private static final Set<String> SAX_PARSER_FACTORY = set( "FallbackIgnoreEntityResolver2", "HardeningException", "HardeningSAXParser", "HardeningSAXParserFactory", "HardeningSAXParserFactory$1", + "HardeningSAXParserFactory$HardeningExpatXMLReader", "HardeningSAXParserFactory$Wrapper", - "HardeningXMLReader", - "SAXParserHardener", - "SAXParserHardener$HardeningExpatXMLReader"); + "HardeningXMLReader"); // @formatter:on // @formatter:off - private static final Set<String> STAX_HARDENER = set( + private static final Set<String> XML_INPUT_FACTORY = set( "FallbackIgnoreXMLResolver", "HardeningException", "HardeningXMLInputFactory", "HardeningXMLInputFactory$1", - "HardeningXMLInputFactory$Wrapper", - "StaxHardener"); + "HardeningXMLInputFactory$Wrapper"); // @formatter:on /** - * TrAX, XPath and schema re-harden their sub-parsers through {@link SAXParserHardener#hardenSource(Source)}, so each builds on the full SAX closure below; - * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM hardener, so + * TrAX, XPath and schema re-harden their sub-parsers through {@link HardeningSAXParserFactory#harden(Source)}, so each builds on the full SAX closure below; + * TrAX additionally parses the Xalan {@code getAssociatedStylesheet} source and XPath its InputSource-taking evaluate calls through the DOM entry point, so * their closures carry that set too. */ // @formatter:off - private static final Set<String> TRANSFORMER_HARDENER = saxParsersHardenerPlus( - "DocumentBuilderHardener", + private static final Set<String> TRANSFORMER_FACTORY = saxParserFactoryPlus( "FallbackIgnoreEntityResolver2", "FallbackIgnoreURIResolver", "HardeningDocumentBuilder", @@ -107,6 +103,7 @@ class ShadingFootprintTest { "HardeningSAXParser", "HardeningSAXParserFactory", "HardeningSAXParserFactory$1", + "HardeningSAXParserFactory$HardeningExpatXMLReader", "HardeningSAXParserFactory$Wrapper", "HardeningTemplates", "HardeningTemplatesHandler", @@ -117,18 +114,14 @@ class ShadingFootprintTest { "HardeningTransformerHandler", "HardeningXMLFilter", "HardeningXMLReader", - "SAXParserHardener", - "SAXParserHardener$HardeningExpatXMLReader", "SaxonProvider", "SaxonProvider$1", "SaxonProvider$HardenedConfiguration", - "SaxonProvider$SaxonProviderConfigurer", - "TransformerHardener"); + "SaxonProvider$SaxonProviderConfigurer"); // @formatter:on // @formatter:off - private static final Set<String> XPATH_HARDENER = saxParsersHardenerPlus( - "DocumentBuilderHardener", + private static final Set<String> XPATH_FACTORY = saxParserFactoryPlus( "FallbackIgnoreEntityResolver2", "FallbackIgnoreURIResolver", "HardeningDocumentBuilder", @@ -139,6 +132,7 @@ class ShadingFootprintTest { "HardeningSAXParser", "HardeningSAXParserFactory", "HardeningSAXParserFactory$1", + "HardeningSAXParserFactory$HardeningExpatXMLReader", "HardeningSAXParserFactory$Wrapper", "HardeningXMLReader", "HardeningXPath", @@ -146,23 +140,21 @@ class ShadingFootprintTest { "HardeningXPathFactory", "HardeningXPathFactory$1", "HardeningXPathFactory$Wrapper", - "SAXParserHardener", - "SAXParserHardener$HardeningExpatXMLReader", "SaxonProvider", "SaxonProvider$1", "SaxonProvider$HardenedConfiguration", - "SaxonProvider$SaxonProviderConfigurer", - "XPathHardener"); + "SaxonProvider$SaxonProviderConfigurer"); // @formatter:on // @formatter:off - private static final Set<String> SCHEMA_HARDENER = saxParsersHardenerPlus( + private static final Set<String> SCHEMA_FACTORY = saxParserFactoryPlus( "FallbackIgnoreEntityResolver2", "FallbackIgnoreLSResourceResolver", "HardeningException", "HardeningSAXParser", "HardeningSAXParserFactory", "HardeningSAXParserFactory$1", + "HardeningSAXParserFactory$HardeningExpatXMLReader", "HardeningSAXParserFactory$Wrapper", "HardeningSchema", "HardeningSchemaFactory", @@ -170,22 +162,19 @@ class ShadingFootprintTest { "HardeningSchemaFactory$Wrapper", "HardeningValidator", "HardeningValidatorHandler", - "HardeningXMLReader", - "SAXParserHardener", - "SAXParserHardener$HardeningExpatXMLReader", - "SchemaHardener"); + "HardeningXMLReader"); // @formatter:on /** * Class count of the {@link #rootClosure()} DOM entry point, the baseline the {@link #reportFootprint()} percentages are computed against. */ - private static final int LIBRARY_CLASS_COUNT = 7; + private static final int LIBRARY_CLASS_COUNT = 6; /** * Entry points reported by the {@link #reportFootprint()} diagnostic, most-focused first, ending with the whole library. */ - private static final String[] REPORTED = {"DocumentBuilderHardener", "SAXParserHardener", "StaxHardener", "TransformerHardener", "XPathHardener", - "SchemaHardener"}; + private static final String[] REPORTED = {"HardeningDocumentBuilderFactory", "HardeningSAXParserFactory", "HardeningXMLInputFactory", + "HardeningTransformerFactory", "HardeningXPathFactory", "HardeningSchemaFactory"}; private static Clazzpath clazzpath; private static Path classesDir; @@ -253,10 +242,10 @@ private static Set<String> rootClosure() { } /** - * {@link #SAX_PARSER_HARDENER} plus the extra names; used where an entry point's closure is the SAX path plus its own classes. + * {@link #SAX_PARSER_FACTORY} plus the extra names; used where an entry point's closure is the SAX path plus its own classes. */ - private static Set<String> saxParsersHardenerPlus(final String... more) { - final Set<String> union = new TreeSet<>(SAX_PARSER_HARDENER); + private static Set<String> saxParserFactoryPlus(final String... more) { + final Set<String> union = new TreeSet<>(SAX_PARSER_FACTORY); union.addAll(Arrays.asList(more)); return union; } @@ -270,8 +259,8 @@ private static String strip(final String qualifiedName) { } @Test - void documentBuilderHardenerFootprint() { - assertEquals(DOCUMENT_BUILDER_HARDENER, closureOf("DocumentBuilderHardener")); + void documentBuilderFactoryFootprint() { + assertEquals(DOCUMENT_BUILDER_FACTORY, closureOf("HardeningDocumentBuilderFactory")); } @Test @@ -280,27 +269,27 @@ void rootClosureMatchesDocumentBuilderFootprint() { } @Test - void saxParserHardenerFootprint() { - assertEquals(SAX_PARSER_HARDENER, closureOf("SAXParserHardener")); + void saxParserFactoryFootprint() { + assertEquals(SAX_PARSER_FACTORY, closureOf("HardeningSAXParserFactory")); } @Test - void schemaHardenerFootprint() { - assertEquals(SCHEMA_HARDENER, closureOf("SchemaHardener")); + void schemaFactoryFootprint() { + assertEquals(SCHEMA_FACTORY, closureOf("HardeningSchemaFactory")); } @Test - void staxHardenerFootprint() { - assertEquals(STAX_HARDENER, closureOf("StaxHardener")); + void xmlInputFactoryFootprint() { + assertEquals(XML_INPUT_FACTORY, closureOf("HardeningXMLInputFactory")); } @Test - void transformerHardenerFootprint() { - assertEquals(TRANSFORMER_HARDENER, closureOf("TransformerHardener")); + void transformerFactoryFootprint() { + assertEquals(TRANSFORMER_FACTORY, closureOf("HardeningTransformerFactory")); } @Test - void xPathHardenerFootprint() { - assertEquals(XPATH_HARDENER, closureOf("XPathHardener")); + void xPathFactoryFootprint() { + assertEquals(XPATH_FACTORY, closureOf("HardeningXPathFactory")); } } diff --git a/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java b/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java index 7d9d01e..23fcfdd 100644 --- a/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java +++ b/src/test/java/org/apache/commons/xml/UnsupportedXmlImplementationTest.java @@ -93,7 +93,7 @@ public void setFeature(final String name, final boolean value) throws SAXNotReco void hardenRejectsUnsecurableFactory() { final IllegalStateException thrown = assertThrows( IllegalStateException.class, - () -> DocumentBuilderHardener.harden(new FakeDocumentBuilderFactory())); + () -> HardeningDocumentBuilderFactory.harden(new FakeDocumentBuilderFactory())); assertNotNull(thrown.getMessage()); assertTrue(thrown.getMessage().contains(FakeDocumentBuilderFactory.class.getName()), "Exception message must name the unsupported class: " + thrown.getMessage()); @@ -103,7 +103,7 @@ void hardenRejectsUnsecurableFactory() { void hardenRejectsUnsecurableSaxFactory() { final IllegalStateException thrown = assertThrows( IllegalStateException.class, - () -> SAXParserHardener.harden(new FakeSAXParserFactory())); + () -> HardeningSAXParserFactory.harden(new FakeSAXParserFactory())); assertNotNull(thrown.getMessage()); assertTrue(thrown.getMessage().contains(FakeSAXParserFactory.class.getName()), "Exception message must name the unsupported class: " + thrown.getMessage()); diff --git a/src/test/java/org/apache/commons/xml/XIncludeTest.java b/src/test/java/org/apache/commons/xml/XIncludeTest.java index 3dbc4ce..64cda94 100644 --- a/src/test/java/org/apache/commons/xml/XIncludeTest.java +++ b/src/test/java/org/apache/commons/xml/XIncludeTest.java @@ -341,7 +341,7 @@ void hardenReaderAllowListResolvesParseXml() throws Exception { final SAXParserFactory unhardenedFactory = SAXParserFactory.newInstance(); unhardenedFactory.setNamespaceAware(true); assumeXIncludeAware(unhardenedFactory); - final XMLReader reader = SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader()); + final XMLReader reader = HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader()); reader.setEntityResolver(new AllowListResolver()); final String captured = captureCharacters(reader, input); assertEquals(RESOLVED_MARKER, captured.trim(), @@ -356,7 +356,7 @@ void hardenReaderBlocksParseText() throws Exception { final SAXParserFactory unhardenedFactory = SAXParserFactory.newInstance(); unhardenedFactory.setNamespaceAware(true); assumeXIncludeAware(unhardenedFactory); - final XMLReader reader = SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader()); + final XMLReader reader = HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader()); final String captured = captureCharacters(reader, input); assertFalse(captured.contains(LEAKED_MARKER), "hardenReader parse=text must resolve the include to empty, not leak; got: " + captured); @@ -371,7 +371,7 @@ void hardenReaderBlocksParseXml() throws Exception { final SAXParserFactory unhardenedFactory = SAXParserFactory.newInstance(); unhardenedFactory.setNamespaceAware(true); assumeXIncludeAware(unhardenedFactory); - final XMLReader reader = SAXParserHardener.hardenReader(unhardenedFactory.newSAXParser().getXMLReader()); + final XMLReader reader = HardeningSAXParserFactory.harden(unhardenedFactory.newSAXParser().getXMLReader()); assertThrows(SAXException.class, () -> reader.parse(input), "hardenReader should block XInclude parse=xml on reader with XInclude already enabled"); }
