This is an automated email from the ASF dual-hosted git repository.

ppkarwasz pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/commons-xml.git

commit b913da1e262ab47ae6e4b8010dd13aacb85bde66
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Thu Apr 23 20:25:22 2026 +0200

    feat: add stock JDK provider
    
    Adds `StockJdkProvider`, which matches the six JAXP factory classes
    shipped inside the JDK.
    
    The commit also adds test to check every possible way external documents 
can be read by a JAXP class.
    
    Assisted-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 pom.xml                                            |   8 +-
 .../xml/factory/internal/AbstractXmlProvider.java  | 136 +++++++++++++
 .../xml/factory/internal/HardeningException.java   |  34 ++++
 .../xml/factory/internal/ProviderRegistry.java     |   4 +-
 .../xml/factory/internal/StockJdkProvider.java     | 110 +++++++++++
 .../org.apache.commons.xml.factory.spi.XmlProvider |   2 +
 .../commons/xml/factory/XmlFactoriesTest.java      | 122 ++++++++++++
 .../xml/factory/attacks/AttackTestSupport.java     | 219 +++++++++++++++++++++
 .../xml/factory/attacks/BillionLaughsTest.java     |  73 +++++++
 .../xml/factory/attacks/ExternalDtdTest.java       |  65 ++++++
 .../factory/attacks/ExternalGeneralEntityTest.java |  67 +++++++
 .../attacks/ExternalParameterEntityTest.java       |  68 +++++++
 .../xml/factory/attacks/ExternalSchemaTest.java    |  69 +++++++
 .../factory/attacks/ExternalStylesheetTest.java    |  66 +++++++
 .../commons/xml/factory/attacks/Payloads.java      |  64 ++++++
 15 files changed, 1102 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index 5b483e7..5b6f678 100644
--- a/pom.xml
+++ b/pom.xml
@@ -75,9 +75,9 @@
         Run the test suite four times, each with exactly one JAXP 
implementation on the classpath:
 
           - test-stockjdk: stock JDK factories only
-          - test-xerces:   stock JDK + external Apache Xerces
-          - test-woodstox: stock JDK + Woodstox (StAX)
-          - test-saxon:    stock JDK + Saxon-HE (TrAX, XPath)
+          - test-xerces:   stock JDK + external Apache Xerces  (stashed until 
XercesProvider lands)
+          - test-woodstox: stock JDK + Woodstox (StAX)          (stashed until 
WoodstoxProvider lands)
+          - test-saxon:    stock JDK + Saxon-HE (TrAX, XPath)   (stashed until 
SaxonProvider lands)
       -->
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -94,6 +94,7 @@
               
<reportsDirectory>${project.build.directory}/surefire-reports/stockjdk</reportsDirectory>
             </configuration>
           </execution>
+          <!--
           <execution>
             <id>test-xerces</id>
             <goals><goal>test</goal></goals>
@@ -136,6 +137,7 @@
               </additionalClassPathDependencies>
             </configuration>
           </execution>
+          -->
         </executions>
       </plugin>
 
diff --git 
a/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java
 
b/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java
new file mode 100644
index 0000000..4837b43
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/internal/AbstractXmlProvider.java
@@ -0,0 +1,136 @@
+/*
+ * 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.factory.internal;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+import javax.xml.xpath.XPathFactoryConfigurationException;
+
+import org.apache.commons.xml.factory.spi.XmlProvider;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
+/**
+ * Common scaffolding for {@link XmlProvider} implementations bundled with 
this library.
+ *
+ * <p>Subclasses pass the fully qualified class names of the factory 
implementations they handle to {@link #AbstractXmlProvider(String...)}. The 
inherited
+ * {@link #supports(Class)} then returns {@code true} for exactly those 
classes, matching on {@link Class#getName()}.</p>
+ *
+ * <p>The class also exposes the JAXP {@code setFeature}/{@code 
setAttribute}/{@code setProperty} helpers every provider uses. Each helper 
wraps the checked
+ * exception documented by the corresponding JAXP setter in a {@link 
HardeningException} whose message names the feature, property or attribute that 
failed.
+ * Every other throwable (including {@link LinkageError} and {@link 
AbstractMethodError}, which typically signal a JAXP implementation on the 
classpath older
+ * than the one this library was compiled against) is left to propagate so the 
caller sees the underlying problem.</p>
+ */
+abstract class AbstractXmlProvider implements XmlProvider {
+
+    private static HardeningException fail(final Object factory, final String 
kind, final String name, final Throwable cause) {
+        return new HardeningException("Failed to set " + kind + " '" + name + 
"' on " + factory.getClass().getName(), cause);
+    }
+
+    static void setAttribute(final DocumentBuilderFactory factory, final 
String attribute, final Object value) {
+        try {
+            factory.setAttribute(attribute, value);
+        } catch (final IllegalArgumentException e) {
+            throw fail(factory, "attribute", attribute, e);
+        }
+    }
+
+    static void setAttribute(final TransformerFactory factory, final String 
attribute, final Object value) {
+        try {
+            factory.setAttribute(attribute, value);
+        } catch (final IllegalArgumentException e) {
+            throw fail(factory, "attribute", attribute, e);
+        }
+    }
+
+    static void setFeature(final DocumentBuilderFactory factory, final String 
feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final ParserConfigurationException e) {
+            throw fail(factory, "feature", feature, e);
+        }
+    }
+
+    static void setFeature(final SAXParserFactory factory, final String 
feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final ParserConfigurationException | 
SAXNotRecognizedException | SAXNotSupportedException e) {
+            throw fail(factory, "feature", feature, e);
+        }
+    }
+
+    static void setFeature(final TransformerFactory factory, final String 
feature, final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final TransformerConfigurationException e) {
+            throw fail(factory, "feature", feature, e);
+        }
+    }
+
+    static void setFeature(final XPathFactory factory, final String feature, 
final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final XPathFactoryConfigurationException e) {
+            throw fail(factory, "feature", feature, e);
+        }
+    }
+
+    static void setFeature(final SchemaFactory factory, final String feature, 
final boolean value) {
+        try {
+            factory.setFeature(feature, value);
+        } catch (final SAXNotRecognizedException | SAXNotSupportedException e) 
{
+            throw fail(factory, "feature", feature, e);
+        }
+    }
+
+    static void setProperty(final XMLInputFactory factory, final String 
property, final Object value) {
+        try {
+            factory.setProperty(property, value);
+        } catch (final IllegalArgumentException e) {
+            throw fail(factory, "property", property, e);
+        }
+    }
+
+    static void setProperty(final SchemaFactory factory, final String 
property, final Object value) {
+        try {
+            factory.setProperty(property, value);
+        } catch (final SAXNotRecognizedException | SAXNotSupportedException e) 
{
+            throw fail(factory, "property", property, e);
+        }
+    }
+    private final Set<String> supported;
+
+    protected AbstractXmlProvider(final String... supportedClassNames) {
+        this.supported = Collections.unmodifiableSet(new 
HashSet<>(Arrays.asList(supportedClassNames)));
+    }
+
+    @Override
+    public final boolean supports(final Class<?> factoryClass) {
+        return factoryClass != null && 
supported.contains(factoryClass.getName());
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java 
b/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java
new file mode 100644
index 0000000..dbfaea9
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/internal/HardeningException.java
@@ -0,0 +1,34 @@
+/*
+ * 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.factory.internal;
+
+/**
+ * Thrown when a provider cannot fully harden a factory.
+ *
+ * <p>The message names the specific feature or property that failed; the 
cause is the original checked or unchecked exception from the JAXP
+ * implementation.</p>
+ *
+ * <p>Package-private by design: callers should catch {@link 
IllegalStateException}, which this extends.</p>
+ */
+class HardeningException extends IllegalStateException {
+
+    private static final long serialVersionUID = 1L;
+
+    HardeningException(final String message, final Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java 
b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java
index 0beb077..954bb68 100644
--- 
a/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java
+++ 
b/src/main/java/org/apache/commons/xml/factory/internal/ProviderRegistry.java
@@ -33,7 +33,7 @@
  * <h2>Dispatch order</h2>
  *
  * <ol>
- *   <li>Bundled providers in a fixed order.</li>
+ *   <li>Bundled providers in a fixed order: {@link StockJdkProvider}, {@link 
XercesProvider}, {@link WoodstoxProvider}, {@link SaxonProvider}.</li>
  *   <li>Third-party providers discovered via {@link ServiceLoader}, in the 
order {@link ServiceLoader} yields them.</li>
  *   <li>If nothing matches, {@link 
UnsupportedXmlImplementationException}.</li>
  * </ol>
@@ -97,7 +97,7 @@ private XmlProvider lookup(final Class<?> factoryClass) {
     }
 
     private static List<XmlProvider> bundledProviders() {
-        return Collections.emptyList();
+        return Collections.singletonList(new StockJdkProvider());
     }
 
     private static Iterable<XmlProvider> serviceLoaderProviders() {
diff --git 
a/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java 
b/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java
new file mode 100644
index 0000000..2b0b51c
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/internal/StockJdkProvider.java
@@ -0,0 +1,110 @@
+/*
+ * 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.factory.internal;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+
+import org.apache.commons.xml.factory.spi.XmlProvider;
+
+/**
+ * {@link XmlProvider} for the stock JDK's JAXP implementation.
+ *
+ * <p>This is the internal fork of Apache Xerces, Xalan and friends shipped 
inside {@code com.sun.org.apache.*} and {@code com.sun.xml.internal.*} 
packages.</p>
+ *
+ * <p>Must be declared {@code public} so {@link java.util.ServiceLoader} can 
load it from {@code META-INF/services/}.</p>
+ */
+public final class StockJdkProvider extends AbstractXmlProvider {
+
+    private static final String FEATURE_DISALLOW_DOCTYPE =
+            "http://apache.org/xml/features/disallow-doctype-decl";;
+
+    /** Default constructor; invoked by {@link java.util.ServiceLoader} and 
the registry. */
+    public StockJdkProvider() {
+        
super("com.sun.org.apache.xerces.internal.jaxp.DocumentBuilderFactoryImpl",
+                "com.sun.org.apache.xerces.internal.jaxp.SAXParserFactoryImpl",
+                "com.sun.xml.internal.stream.XMLInputFactoryImpl",
+                
"com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl",
+                "com.sun.org.apache.xpath.internal.jaxp.XPathFactoryImpl",
+                
"com.sun.org.apache.xerces.internal.jaxp.validation.XMLSchemaFactory");
+    }
+
+    @Override
+    public void configure(final DocumentBuilderFactory factory) {
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        setFeature(factory, FEATURE_DISALLOW_DOCTYPE, true);
+        factory.setXIncludeAware(false);
+        factory.setValidating(false);
+        // Defence-in-depth: disallow-doctype-decl and validating=false 
already block external resolution on the normal parse path. These properties 
only fire
+        // if the caller re-enables DOCTYPE, turns validation on, attaches a 
Schema, or registers an EntityResolver that returns null instead of throwing; 
the
+        // default resolver then consults them and refuses the fetch before 
any I/O.
+        //   - ACCESS_EXTERNAL_DTD:    protocols allowed for external DTDs 
declared by DOCTYPE SYSTEM/PUBLIC.
+        //   - ACCESS_EXTERNAL_SCHEMA: protocols allowed for 
xsi:schemaLocation hints and for xs:import/xs:include/xs:redefine in a 
validating Schema.
+        setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+    }
+
+    @Override
+    public void configure(final SAXParserFactory factory) {
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        setFeature(factory, FEATURE_DISALLOW_DOCTYPE, true);
+        factory.setXIncludeAware(false);
+        factory.setValidating(false);
+        // No ACCESS_EXTERNAL_* here: the JAXP SAXParserFactory API exposes 
neither setAttribute nor setProperty, so these properties cannot be applied at
+        // factory level. disallow-doctype-decl and validating=false already 
block external resolution on the normal parse path; callers who modify the 
posture
+        // should set 
parser.setProperty(XMLConstants.ACCESS_EXTERNAL_{DTD,SCHEMA}, "") on each 
SAXParser returned by newSAXParser().
+    }
+
+    @Override
+    public void configure(final XMLInputFactory factory) {
+        setProperty(factory, XMLInputFactory.SUPPORT_DTD, false);
+        setProperty(factory, XMLInputFactory.IS_VALIDATING, false);
+    }
+
+    @Override
+    public void configure(final TransformerFactory factory) {
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Defence-in-depth: FSP already tightens these to the empty string on 
the JDK's XMLSecurityManager path. They only fire if the caller turns FSP off
+        // on the returned factory; the default resolver then consults them 
and refuses the fetch before any I/O.
+        //   - ACCESS_EXTERNAL_DTD:        protocols allowed for external DTDs 
referenced by DOCTYPE SYSTEM in the stylesheet or XML input.
+        //   - ACCESS_EXTERNAL_STYLESHEET: protocols allowed for xsl:import, 
xsl:include and document() URIs during compile and transform.
+        setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
+    }
+
+    @Override
+    public void configure(final XPathFactory factory) {
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+    }
+
+    @Override
+    public void configure(final SchemaFactory factory) {
+        setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Defence-in-depth: FSP already tightens these to the empty string on 
the JDK's XMLSecurityManager path. They only fire if the caller turns FSP off
+        // on the returned factory; the default resolver then consults them 
and refuses the fetch before any I/O. The settings propagate into Validators
+        // created from the compiled Schema.
+        //   - ACCESS_EXTERNAL_DTD:    protocols allowed for external DTDs 
declared by DOCTYPE SYSTEM in the schema or in a validated instance.
+        //   - ACCESS_EXTERNAL_SCHEMA: protocols allowed for xs:import, 
xs:include and xs:redefine schemaLocation URIs during schema compilation.
+        setProperty(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        setProperty(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
+    }
+}
diff --git 
a/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider
 
b/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider
new file mode 100644
index 0000000..667471d
--- /dev/null
+++ 
b/src/main/resources/META-INF/services/org.apache.commons.xml.factory.spi.XmlProvider
@@ -0,0 +1,2 @@
+# SPDX-License-Identifier: Apache-2.0
+org.apache.commons.xml.factory.internal.StockJdkProvider
diff --git a/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java 
b/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java
new file mode 100644
index 0000000..d185c28
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/factory/XmlFactoriesTest.java
@@ -0,0 +1,122 @@
+/*
+ * 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.factory;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.StringReader;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.SAXParserFactory;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.TransformerFactory;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.xpath.XPathFactory;
+
+import org.junit.jupiter.api.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+/**
+ * Public-API smoke tests for {@link XmlFactories}.
+ *
+ * <p>Attack tests live in the {@code attacks} sub-package; this file only 
verifies that fresh factories are returned, that they report safe defaults, and 
that
+ * a benign document still parses successfully.</p>
+ */
+class XmlFactoriesTest {
+
+    private static final String BENIGN_XML =
+            "<?xml version=\"1.0\"?>\n<root><child>hello</child></root>\n";
+
+    @Test
+    void newDocumentBuilderFactoryReturnsFreshInstance() {
+        final DocumentBuilderFactory a = 
XmlFactories.newDocumentBuilderFactory();
+        final DocumentBuilderFactory b = 
XmlFactories.newDocumentBuilderFactory();
+        assertNotNull(a);
+        assertNotNull(b);
+        assertNotSame(a, b);
+    }
+
+    @Test
+    void newDocumentBuilderFactoryDisablesXIncludeAndValidation() {
+        final DocumentBuilderFactory factory = 
XmlFactories.newDocumentBuilderFactory();
+        assertFalse(factory.isXIncludeAware(), "XInclude must be off by 
default");
+        assertFalse(factory.isValidating(), "Validation must be off by 
default");
+    }
+
+    @Test
+    void newDocumentBuilderFactoryEnablesSecureProcessing() throws Exception {
+        final DocumentBuilderFactory factory = 
XmlFactories.newDocumentBuilderFactory();
+        assertTrue(factory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING),
+                "FEATURE_SECURE_PROCESSING must be on");
+    }
+
+    @Test
+    void benignDocumentParses() throws Exception {
+        final Document doc = 
XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(new 
InputSource(new StringReader(BENIGN_XML)));
+        assertNotNull(doc);
+        assertNotNull(doc.getDocumentElement());
+    }
+
+    @Test
+    void newSAXParserFactoryReturnsFreshInstance() {
+        final SAXParserFactory a = XmlFactories.newSAXParserFactory();
+        final SAXParserFactory b = XmlFactories.newSAXParserFactory();
+        assertNotSame(a, b);
+        assertFalse(a.isValidating());
+        assertFalse(a.isXIncludeAware());
+    }
+
+    @Test
+    void newXMLInputFactoryReturnsFreshInstance() {
+        final XMLInputFactory a = XmlFactories.newXMLInputFactory();
+        final XMLInputFactory b = XmlFactories.newXMLInputFactory();
+        assertNotSame(a, b);
+        assertEquals(Boolean.FALSE, 
a.getProperty(XMLInputFactory.SUPPORT_DTD));
+        assertEquals(Boolean.FALSE, 
a.getProperty(XMLInputFactory.IS_VALIDATING));
+    }
+
+    @Test
+    void newTransformerFactoryReturnsFreshInstance() {
+        final TransformerFactory a = XmlFactories.newTransformerFactory();
+        final TransformerFactory b = XmlFactories.newTransformerFactory();
+        assertNotSame(a, b);
+    }
+
+    @Test
+    void newXPathFactoryReturnsFreshInstance() throws Exception {
+        final XPathFactory a = XmlFactories.newXPathFactory();
+        final XPathFactory b = XmlFactories.newXPathFactory();
+        assertNotSame(a, b);
+        assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+    @Test
+    void newSchemaFactoryReturnsFreshInstance() throws Exception {
+        final SchemaFactory a = XmlFactories.newSchemaFactory();
+        final SchemaFactory b = XmlFactories.newSchemaFactory();
+        assertNotSame(a, b);
+        assertTrue(a.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING));
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java 
b/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java
new file mode 100644
index 0000000..b5c90e4
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/AttackTestSupport.java
@@ -0,0 +1,219 @@
+/*
+ * 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.factory.attacks;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Locale;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.stream.StreamResult;
+import javax.xml.transform.stream.StreamSource;
+
+import org.apache.commons.xml.factory.XmlFactories;
+import org.xml.sax.InputSource;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * Shared fixtures for attack tests.
+ *
+ * <p>For each factory type the helpers parse the supplied payload and assert 
that either the parser throws a hardening-style exception or completes without
+ * any attempt to resolve an external resource.</p>
+ */
+final class AttackTestSupport {
+
+    /** Functional interface so test bodies can throw checked JAXP exceptions. 
*/
+    @FunctionalInterface
+    interface ThrowingAction {
+        void run() throws Exception;
+    }
+
+    /**
+     * Keywords (lowercase) that indicate the exception comes from the 
hardening layer or the parser's refusal to handle a DOCTYPE/external reference, 
rather
+     * than from a successful-but-late fetch.
+     */
+    private static final Set<String> HARDENING_KEYWORDS = 
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
+            "doctype", "dtd", "disallow", "external", "entity", "entities", 
"secure", "access is not allowed",
+            "support_dtd", "feature_secure_processing", "prohibited")));
+
+    /**
+     * Upper bound on elapsed time for any payload to be processed.
+     *
+     * <p>A hardened parser either rejects the DOCTYPE up-front (microseconds) 
or silently skips the DTD (milliseconds). A parser that actually attempts a
+     * network fetch would either block on DNS/TCP timeouts or fail with a 
connection error: both incompatible with this budget.</p>
+     */
+    private static final long MAX_MILLIS = 2_000L;
+
+    /**
+     * Asserts that the supplied action threw an exception quickly and the 
exception chain indicates the hardening layer (not a network/file attempt).
+     */
+    private static void assertAttackBlocked(final ThrowingAction action) {
+        final long start = System.nanoTime();
+        Throwable thrown = null;
+        try {
+            action.run();
+        } catch (final Throwable t) {
+            thrown = t;
+        }
+        final long elapsedMs = (System.nanoTime() - start) / 1_000_000L;
+        if (thrown == null) {
+            fail("Expected hardening to reject the attack, but parsing 
completed successfully.");
+            return;
+        }
+        if (elapsedMs >= MAX_MILLIS) {
+            fail(String.format("Hardening rejected the payload only after 
%dms: that suggests an actual resolution attempt rather than an up-front 
block.",
+                    elapsedMs), thrown);
+        }
+        if (!messageOrCauseContainsHardeningKeyword(thrown)) {
+            fail("Exception does not look like it came from the hardening 
layer.", thrown);
+        }
+    }
+
+    static void assertDomBlocks(final String payload) {
+        assertAttackBlocked(() -> 
XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)));
+    }
+
+    static void assertSaxBlocks(final String payload) {
+        assertAttackBlocked(() -> {
+            final XMLReader reader = 
XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader();
+            reader.setContentHandler(new DefaultHandler());
+            reader.setErrorHandler(new DefaultHandler());
+            reader.parse(inputSource(payload));
+        });
+    }
+
+    /**
+     * Asserts that compiling the given schema via {@code 
SchemaFactory.newSchema(Source)} is blocked by the hardening layer.
+     */
+    static void assertSchemaCompilationBlocks(final String xsd) {
+        assertAttackBlocked(() -> 
XmlFactories.newSchemaFactory().newSchema(source(xsd)));
+    }
+
+    /**
+     * Asserts that a StAX parse of the supplied payload blocks DTD resolution.
+     *
+     * <p>StAX with {@code SUPPORT_DTD=false} either throws on entity 
references in content (Billion Laughs, external general entity) or silently 
skips the
+     * DTD. For the benign-body payloads the skipped DTD never becomes an 
error, so instead of requiring an exception we plant a counting {@code 
XMLResolver}
+     * and assert it is never invoked.</p>
+     */
+    static void assertStaxBlocks(final String payload) {
+        final AtomicInteger resolutions = new AtomicInteger();
+        final long start = System.nanoTime();
+        Throwable thrown = null;
+        try {
+            final XMLInputFactory factory = XmlFactories.newXMLInputFactory();
+            factory.setXMLResolver((publicID, systemID, baseURI, namespace) -> 
{
+                resolutions.incrementAndGet();
+                return new ByteArrayInputStream(new byte[0]);
+            });
+            final XMLStreamReader stream = factory.createXMLStreamReader(new 
StringReader(payload));
+            try {
+                while (stream.hasNext()) {
+                    stream.next();
+                }
+            } finally {
+                stream.close();
+            }
+            final XMLEventReader events = factory.createXMLEventReader(new 
StringReader(payload));
+            try {
+                while (events.hasNext()) {
+                    events.nextEvent();
+                }
+            } finally {
+                events.close();
+            }
+        } catch (final Throwable t) {
+            thrown = t;
+        }
+        final long elapsedMs = (System.nanoTime() - start) / 1_000_000L;
+        if (elapsedMs >= MAX_MILLIS) {
+            fail(String.format("StAX parse took %dms: suggests an actual 
external resolution attempt.", elapsedMs));
+        }
+        assertEquals(0, resolutions.get(), String.format(
+                "StAX parser invoked the external resolver %d time(s); 
hardening failed to block DTD processing. Exception (if any): %s",
+                resolutions.get(),
+                thrown));
+    }
+
+    /**
+     * Asserts that compiling the given stylesheet via {@code 
TransformerFactory.newTransformer(Source)} is blocked by the hardening layer.
+     */
+    static void assertStylesheetCompilationBlocks(final String xslt) {
+        assertAttackBlocked(() -> 
XmlFactories.newTransformerFactory().newTransformer(source(xslt)));
+    }
+
+    static void assertTransformerBlocks(final String payload) {
+        assertAttackBlocked(() -> 
XmlFactories.newTransformerFactory().newTransformer().transform(source(payload),
 new StreamResult(new StringWriter())));
+    }
+
+    /**
+     * Asserts that compiling the given stylesheet and then transforming the 
given input through it is blocked by the hardening layer. Used for attacks whose
+     * external fetch is triggered at {@code Transformer.transform(...)} time 
rather than compile time (for example {@code document()} in an XSLT template).
+     */
+    static void assertTransformerBlocksWithStylesheet(final String xslt, final 
String input) {
+        assertAttackBlocked(() -> XmlFactories.newTransformerFactory()
+                .newTransformer(source(xslt))
+                .transform(source(input), new StreamResult(new 
StringWriter())));
+    }
+
+    /**
+     * Asserts that validating the given XML instance through a {@link 
javax.xml.validation.Validator} obtained from {@link Payloads#BENIGN_SCHEMA} is 
blocked
+     * by the hardening layer. The schema itself is trusted and benign; the 
attack lives in the instance document.
+     */
+    static void assertValidatorBlocks(final String xml) {
+        assertAttackBlocked(() -> 
XmlFactories.newSchemaFactory().newSchema(source(Payloads.BENIGN_SCHEMA)).newValidator().validate(source(xml)));
+    }
+
+    private static InputSource inputSource(final String xml) {
+        return new InputSource(new StringReader(xml));
+    }
+
+    private static boolean messageOrCauseContainsHardeningKeyword(final 
Throwable t) {
+        Throwable current = t;
+        while (current != null) {
+            final String msg = current.getMessage();
+            if (msg != null) {
+                final String lower = msg.toLowerCase(Locale.ROOT);
+                if (HARDENING_KEYWORDS.stream().anyMatch(lower::contains)) {
+                    return true;
+                }
+            }
+            current = current.getCause();
+        }
+        return false;
+    }
+
+    private static StreamSource source(final String xml) {
+        return new StreamSource(new StringReader(xml));
+    }
+
+    private AttackTestSupport() {
+    }
+
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java 
b/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java
new file mode 100644
index 0000000..76e0bb1
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/BillionLaughsTest.java
@@ -0,0 +1,73 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+class BillionLaughsTest {
+
+    private static final String INSERTION = "&lol6;";
+
+    private static String withDoctype(final String rootQName, final String 
body) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE " + rootQName + " [\n"
+                + "  <!ENTITY lol \"lol\">\n"
+                + "  <!ENTITY lol1 
\"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n"
+                + "  <!ENTITY lol2 
\"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n"
+                + "  <!ENTITY lol3 
\"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n"
+                + "  <!ENTITY lol4 
\"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n"
+                + "  <!ENTITY lol5 
\"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n"
+                + "  <!ENTITY lol6 
\"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n"
+                + "]>\n"
+                + body + "\n";
+    }
+
+    @Test
+    void domBlocksBillionLaughs() {
+        AttackTestSupport.assertDomBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void saxBlocksBillionLaughs() {
+        AttackTestSupport.assertSaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void schemaFactoryBlocksBillionLaughs() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", 
Payloads.xsdBody(INSERTION)));
+    }
+
+    @Test
+    void staxBlocksBillionLaughs() {
+        AttackTestSupport.assertStaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerBlocksBillionLaughs() {
+        AttackTestSupport.assertTransformerBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerStylesheetBlocksBillionLaughs() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet",
 Payloads.xsltBody(INSERTION)));
+    }
+
+    @Test
+    void validatorBlocksBillionLaughs() {
+        AttackTestSupport.assertValidatorBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java
new file mode 100644
index 0000000..db9965b
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalDtdTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+class ExternalDtdTest {
+
+    private static final String INSERTION = "";
+
+    private static String withDoctype(final String rootQName, final String 
body) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE " + rootQName + " SYSTEM \"" + 
Payloads.UNREACHABLE_HTTP + "\">\n"
+                + body + "\n";
+    }
+
+    @Test
+    void domBlocks() {
+        AttackTestSupport.assertDomBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void saxBlocks() {
+        AttackTestSupport.assertSaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void schemaFactoryBlocks() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", 
Payloads.xsdBody(INSERTION)));
+    }
+
+    @Test
+    void staxBlocks() {
+        AttackTestSupport.assertStaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerBlocks() {
+        AttackTestSupport.assertTransformerBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerStylesheetBlocks() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet",
 Payloads.xsltBody(INSERTION)));
+    }
+
+    @Test
+    void validatorBlocks() {
+        AttackTestSupport.assertValidatorBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java
 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java
new file mode 100644
index 0000000..d0cacf9
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalGeneralEntityTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+class ExternalGeneralEntityTest {
+
+    private static final String INSERTION = "&xxe;";
+
+    private static String withDoctype(final String rootQName, final String 
body) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE " + rootQName + " [\n"
+                + "  <!ENTITY xxe SYSTEM \"" + Payloads.UNREACHABLE_FILE + 
"\">\n"
+                + "]>\n"
+                + body + "\n";
+    }
+
+    @Test
+    void domBlocks() {
+        AttackTestSupport.assertDomBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void saxBlocks() {
+        AttackTestSupport.assertSaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void schemaFactoryBlocks() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", 
Payloads.xsdBody(INSERTION)));
+    }
+
+    @Test
+    void staxBlocks() {
+        AttackTestSupport.assertStaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerBlocks() {
+        AttackTestSupport.assertTransformerBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerStylesheetBlocks() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet",
 Payloads.xsltBody(INSERTION)));
+    }
+
+    @Test
+    void validatorBlocks() {
+        AttackTestSupport.assertValidatorBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java
 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java
new file mode 100644
index 0000000..b1f853c
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalParameterEntityTest.java
@@ -0,0 +1,68 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+class ExternalParameterEntityTest {
+
+    private static final String INSERTION = "";
+
+    private static String withDoctype(final String rootQName, final String 
body) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<!DOCTYPE " + rootQName + " [\n"
+                + "  <!ENTITY % xxe SYSTEM \"" + Payloads.UNREACHABLE_HTTP + 
"\">\n"
+                + "  %xxe;\n"
+                + "]>\n"
+                + body + "\n";
+    }
+
+    @Test
+    void domBlocks() {
+        AttackTestSupport.assertDomBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void saxBlocks() {
+        AttackTestSupport.assertSaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void schemaFactoryBlocks() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(withDoctype("xs:schema", 
Payloads.xsdBody(INSERTION)));
+    }
+
+    @Test
+    void staxBlocks() {
+        AttackTestSupport.assertStaxBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerBlocks() {
+        AttackTestSupport.assertTransformerBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+
+    @Test
+    void transformerStylesheetBlocks() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(withDoctype("xsl:stylesheet",
 Payloads.xsltBody(INSERTION)));
+    }
+
+    @Test
+    void validatorBlocks() {
+        AttackTestSupport.assertValidatorBlocks(withDoctype("root", 
Payloads.xmlBody(INSERTION)));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java
new file mode 100644
index 0000000..b394a4c
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalSchemaTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exercises {@link javax.xml.XMLConstants#ACCESS_EXTERNAL_SCHEMA 
ACCESS_EXTERNAL_SCHEMA} via the three XSD compile-time vectors it guards:
+ * {@code xs:import}, {@code xs:include} and {@code xs:redefine}.
+ */
+class ExternalSchemaTest {
+
+    private static final String IMPORT_NAMESPACE = 
"http://example.org/external";;
+
+    private static String xsdWithImport(final String namespace, final String 
schemaLocation) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>\n"
+                + "  <xs:import namespace=\"" + namespace + "\" 
schemaLocation=\"" + schemaLocation + "\"/>\n"
+                + "  <xs:element name=\"root\" type=\"xs:string\"/>\n"
+                + "</xs:schema>\n";
+    }
+
+    private static String xsdWithInclude(final String schemaLocation) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>\n"
+                + "  <xs:include schemaLocation=\"" + schemaLocation + "\"/>\n"
+                + "  <xs:element name=\"root\" type=\"xs:string\"/>\n"
+                + "</xs:schema>\n";
+    }
+
+    private static String xsdWithRedefine(final String schemaLocation) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>\n"
+                + "  <xs:redefine schemaLocation=\"" + schemaLocation + "\">\n"
+                + "    <xs:complexType 
name=\"Placeholder\"><xs:sequence/></xs:complexType>\n"
+                + "  </xs:redefine>\n"
+                + "  <xs:element name=\"root\" type=\"xs:string\"/>\n"
+                + "</xs:schema>\n";
+    }
+
+    @Test
+    void schemaFactoryBlocksImport() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(xsdWithImport(IMPORT_NAMESPACE, 
Payloads.UNREACHABLE_HTTP));
+    }
+
+    @Test
+    void schemaFactoryBlocksInclude() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(xsdWithInclude(Payloads.UNREACHABLE_HTTP));
+    }
+
+    @Test
+    void schemaFactoryBlocksRedefine() {
+        
AttackTestSupport.assertSchemaCompilationBlocks(xsdWithRedefine(Payloads.UNREACHABLE_HTTP));
+    }
+}
diff --git 
a/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java
 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java
new file mode 100644
index 0000000..f35842e
--- /dev/null
+++ 
b/src/test/java/org/apache/commons/xml/factory/attacks/ExternalStylesheetTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.factory.attacks;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Exercises {@link javax.xml.XMLConstants#ACCESS_EXTERNAL_STYLESHEET 
ACCESS_EXTERNAL_STYLESHEET} via the three XSLT vectors it guards:
+ * {@code xsl:include} and {@code xsl:import} at compile time, and the {@code 
document()} function at transform time.
+ */
+class ExternalStylesheetTest {
+
+    private static final String INPUT = "<root/>";
+
+    private static String xsltWithDocumentCall(final String uri) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xsl:stylesheet version=\"1.0\" 
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\";>\n"
+                + "  <xsl:template match=\"/\">\n"
+                + "    <xsl:copy-of select=\"document('" + uri + "')\"/>\n"
+                + "  </xsl:template>\n"
+                + "</xsl:stylesheet>\n";
+    }
+
+    private static String xsltWithImport(final String href) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xsl:stylesheet version=\"1.0\" 
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\";>\n"
+                + "  <xsl:import href=\"" + href + "\"/>\n"
+                + "</xsl:stylesheet>\n";
+    }
+
+    private static String xsltWithInclude(final String href) {
+        return "<?xml version=\"1.0\"?>\n"
+                + "<xsl:stylesheet version=\"1.0\" 
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\";>\n"
+                + "  <xsl:include href=\"" + href + "\"/>\n"
+                + "</xsl:stylesheet>\n";
+    }
+
+    @Test
+    void transformerBlocksDocument() {
+        
AttackTestSupport.assertTransformerBlocksWithStylesheet(xsltWithDocumentCall(Payloads.UNREACHABLE_HTTP),
 INPUT);
+    }
+
+    @Test
+    void transformerStylesheetBlocksImport() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(xsltWithImport(Payloads.UNREACHABLE_HTTP));
+    }
+
+    @Test
+    void transformerStylesheetBlocksInclude() {
+        
AttackTestSupport.assertStylesheetCompilationBlocks(xsltWithInclude(Payloads.UNREACHABLE_HTTP));
+    }
+}
diff --git a/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java 
b/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java
new file mode 100644
index 0000000..a16a01a
--- /dev/null
+++ b/src/test/java/org/apache/commons/xml/factory/attacks/Payloads.java
@@ -0,0 +1,64 @@
+/*
+ * 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.factory.attacks;
+
+/**
+ * Shared building blocks for attack-test payloads: unreachable-URI constants, 
benign reference documents, and body-shape helpers for XML, XSLT and XSD.
+ *
+ * <p>Each attack test class combines one of the body-shape helpers with an 
attack-specific DOCTYPE prologue of its own.</p>
+ */
+final class Payloads {
+
+    /** Trivial W3C XML Schema that validates {@link #xmlBody(String)} output. 
*/
+    static final String BENIGN_SCHEMA =
+            "<?xml version=\"1.0\"?>\n"
+            + "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>\n"
+            + "  <xs:element name=\"root\">\n"
+            + "    <xs:complexType>\n"
+            + "      <xs:sequence>\n"
+            + "        <xs:element name=\"child\" type=\"xs:string\"/>\n"
+            + "      </xs:sequence>\n"
+            + "    </xs:complexType>\n"
+            + "  </xs:element>\n"
+            + "</xs:schema>\n";
+
+    /** A file URI that cannot exist on any reasonable filesystem. */
+    static final String UNREACHABLE_FILE = 
"file:/dev/null/nonexistent/evil.dtd";
+
+    /** An unreachable HTTP URI: TCP connect to localhost:1 rejects quickly. */
+    static final String UNREACHABLE_HTTP = "http://127.0.0.1:1/evil.dtd";;
+
+    private Payloads() {
+    }
+
+    static String xmlBody(final String text) {
+        return "<root><child>" + text + "</child></root>";
+    }
+
+    static String xsltBody(final String text) {
+        return "<xsl:stylesheet version=\"1.0\" 
xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\";>\n"
+                + "  <xsl:template match=\"/\">" + text + "</xsl:template>\n"
+                + "</xsl:stylesheet>";
+    }
+
+    static String xsdBody(final String text) {
+        return "<xs:schema xmlns:xs=\"http://www.w3.org/2001/XMLSchema\";>\n"
+                + "  <xs:annotation><xs:documentation>" + text + 
"</xs:documentation></xs:annotation>\n"
+                + "  <xs:element name=\"root\" type=\"xs:string\"/>\n"
+                + "</xs:schema>";
+    }
+}

Reply via email to