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 9d485e3ca3412002219134ee2be5d4ba1b101ebf
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Wed Apr 29 21:37:27 2026 +0200

    Add Android tests and rationalise test suite (#10)
    
    CI gains a `build-android` job that runs the existing instrumented suite on 
API 33 and 35 emulators alongside the JVM matrix. Getting it green required 
pinning down what hardening guarantees parsers offer regardless of which JAXP 
surface a payload reaches them through.
    
    After this change a payload behaves the same whether it is parsed as an XML 
document, an XSD schema or an XSLT stylesheet:
    
    - A `DOCTYPE` that names an external subset parses without throwing because 
the subset is silently skipped.
    - An undeclared general entity reference is silently dropped per XML 1.0 
§4.1.
    - An external entity reference is rejected through whatever channel the 
underlying parser exposes, with one exception: on Android Expat the JNI bridge 
does not enable libexpat's `XML_SetParamEntityParsing`, so external parameter 
entities are silently skipped without consulting the resolver; the affected 
test cases are gated on a probe that detects this.
    
    Making the contract uniform required wrapping the JDK's 
`TransformerFactory` and `SchemaFactory` so the SAX reader they use internally 
is the one `XmlFactories.newSAXParserFactory()` hands out. Their public API 
does not propagate the features that matter (`load-external-dtd` in particular) 
down to the SAX layer, so without the wrap the inner reader runs with its own 
defaults. Routing every `Source` through the same hardened reader also drops 
the Apache-Xalan and Saxon dependency on `X [...]
    
    Assisted-By: Claude Opus 4.7 (1M context) <[email protected]>
---
 .github/workflows/maven.yml                        | 103 +++-
 android-tests/build.gradle.kts                     |   4 +-
 .../commons/xml/factory/AndroidProvider.java       |  98 ++--
 .../commons/xml/factory/DelegatingSAXParser.java   |  87 ++++
 .../commons/xml/factory/DelegatingXMLReader.java   | 111 +++++
 .../commons/xml/factory/HardeningSchema.java       |  51 ++
 .../xml/factory/HardeningSchemaFactory.java        |  79 +++
 .../commons/xml/factory/HardeningTemplates.java    |  52 ++
 .../commons/xml/factory/HardeningTransformer.java  |  43 ++
 .../xml/factory/HardeningTransformerFactory.java   |  89 ++++
 .../commons/xml/factory/HardeningValidator.java    |  97 ++++
 .../apache/commons/xml/factory/SaxonProvider.java  |   3 +
 .../commons/xml/factory/StockJdkProvider.java      |  23 +-
 .../apache/commons/xml/factory/XalanProvider.java  | 144 +-----
 .../apache/commons/xml/factory/XercesProvider.java |  93 +---
 .../apache/commons/xml/factory/XmlFactories.java   |  30 ++
 .../commons/xml/factory/AttackTestSupport.java     | 542 ++++++++++++++-------
 .../commons/xml/factory/BillionLaughsTest.java     | 154 +++---
 .../commons/xml/factory/DoctypeOnlyTest.java       |   3 +
 .../commons/xml/factory/ExternalDtdTest.java       |  36 +-
 .../xml/factory/ExternalGeneralEntityTest.java     |  24 +-
 .../xml/factory/ExternalParameterEntityTest.java   |  32 +-
 .../commons/xml/factory/SchemaImportTest.java      |   2 +-
 .../commons/xml/factory/SchemaIncludeTest.java     |   2 +-
 .../commons/xml/factory/SchemaRedefineTest.java    |   2 +-
 25 files changed, 1366 insertions(+), 538 deletions(-)

diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 8115045..f757817 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -82,7 +82,9 @@ jobs:
       - name: Set up Maven cache
         uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae  #v5.0.5
         with:
-          path: ~/.m2/repository
+          path: |
+            ~/.m2/repository
+            ~/.gradle/wrapper
           key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
           restore-keys: |
             ${{ runner.os }}-maven-
@@ -97,4 +99,103 @@ jobs:
           name: surefire-reports-${{ matrix.os }}-jdk${{ matrix.java-version }}
           path: '**/target/surefire-reports/'
           if-no-files-found: ignore
+          retention-days: 14
+
+  build-android:
+
+    # API 33 mirrors the Pixel 6a AVD baseline configured in 
android-tests/build.gradle.kts; API 35 covers the latest stable platform.
+    # Linux runners only: GitHub-hosted macOS / Windows runners no longer ship 
KVM / HAXM acceleration, and arm64 / x86 emulator images
+    # without acceleration boot too slowly to be useful in CI.
+    name: build (android, API ${{ matrix.api-level }})
+    runs-on: ubuntu-latest
+    timeout-minutes: 45
+    strategy:
+      fail-fast: false
+      matrix:
+        api-level: [ 33, 35 ]
+
+    steps:
+      - name: Enable KVM
+        run: |
+          echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", 
OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+          sudo udevadm control --reload-rules
+          sudo udevadm trigger --name-match=kvm
+
+      - name: Checkout repository
+        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # 
v6.0.2
+        with:
+          persist-credentials: false
+
+      - name: Set up JDK 17
+        uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # 
v5.2.0
+        with:
+          distribution: temurin
+          java-version: 17
+
+      # We only restore, since the JDK build's cache will be more comprehensive
+      - name: Set up Maven cache
+        uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # 
v5.0.5
+        with:
+          path: |
+            ~/.m2/repository
+          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
+          restore-keys: |
+            ${{ runner.os }}-maven-
+
+      - name: Build library JAR
+        run: mvn --errors --show-version --batch-mode --no-transfer-progress 
-DskipTests package
+
+      - name: AVD cache
+        id: avd-cache
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+        with:
+          path: |
+            ~/.android/avd
+            ~/.android/adb*
+          key: avd-${{ matrix.api-level }}
+
+      # Only runs on a cache miss to create an AVD snapshot and speed up the 
next builds
+      - name: Generate AVD snapshot
+        if: steps.avd-cache.outputs.cache-hit != 'true'
+        uses: 
reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a 
# v2.37.0
+        with:
+          api-level: ${{ matrix.api-level }}
+          target: default
+          arch: x86_64
+          force-avd-creation: false
+          emulator-options: -no-window -gpu swiftshader_indirect -noaudio 
-no-boot-anim -camera-back none
+          disable-animations: false
+          script: echo "Generated AVD snapshot"
+
+      - name: Gradle cache
+        uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+        with:
+          path: |
+            ~/.gradle/caches
+            ~/.gradle/wrapper
+          key: ${{ runner.os }}-gradle-${{ 
hashFiles('android-tests/*.gradle.kts', 
'android-tests/gradle/wrapper/gradle-wrapper.properties') }}
+          restore-keys: |
+            ${{ runner.os }}-gradle-
+
+      - name: Run instrumented tests
+        uses: 
reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a 
# v2.37.0
+        with:
+          api-level: ${{ matrix.api-level }}
+          target: default
+          arch: x86_64
+          force-avd-creation: false
+          emulator-options: -no-snapshot-save -no-window -gpu 
swiftshader_indirect -noaudio -no-boot-anim -camera-back none
+          disable-animations: true
+          working-directory: android-tests
+          script: ./gradlew connectedDebugAndroidTest
+
+      - name: Upload Android test reports
+        if: failure()
+        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
# v7.0.1
+        with:
+          name: android-test-reports-api${{ matrix.api-level }}
+          path: |
+            android-tests/build/outputs/androidTest-results/
+            android-tests/build/reports/androidTests/
+          if-no-files-found: ignore
           retention-days: 14
\ No newline at end of file
diff --git a/android-tests/build.gradle.kts b/android-tests/build.gradle.kts
index 18a7c15..94acbbf 100644
--- a/android-tests/build.gradle.kts
+++ b/android-tests/build.gradle.kts
@@ -18,7 +18,7 @@ import com.android.build.api.dsl.ManagedVirtualDevice
 
 plugins {
     id("com.android.library") version "8.6.1"
-    id("de.mannodermaus.android-junit") version "2.0.1"
+    id("de.mannodermaus.android-junit5") version "1.14.0.0"
 }
 
 val libraryVersion = "0.1.0-SNAPSHOT"
@@ -66,7 +66,7 @@ android {
 junitPlatform {
     filters {
         // Pass single tag expression
-        includeTags("dom | sax | schema")
+        includeTags("dom | sax | schema | trax")
     }
 }
 
diff --git a/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java 
b/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java
index f2863ae..dd46fc2 100644
--- a/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java
+++ b/src/main/java/org/apache/commons/xml/factory/AndroidProvider.java
@@ -22,12 +22,16 @@
 import javax.xml.parsers.DocumentBuilder;
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
 
 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;
+import org.xml.sax.ext.DefaultHandler2;
 import org.xml.sax.ext.LexicalHandler;
 
 /**
@@ -72,37 +76,22 @@ final class AndroidProvider {
      *
      * <p>Merely <em>declaring</em> an external subset does not cause the 
parse to throw.</p>
      */
-    private static final class DtdAwareDenyResolver implements EntityResolver, 
LexicalHandler {
-
-        private boolean inDtd;
-        private String dtdPublicId;
-        private String dtdSystemId;
+    private static final class DtdAwareDenyResolver extends DefaultHandler2 {
 
         private static String forbiddenMessage(final String publicId, final 
String systemId) {
             return String.format("External Entity: failed to read external 
entity (publicId='%s', systemId='%s'); external entity access is denied.",
                     publicId, systemId);
         }
 
-        @Override
-        public void comment(final char[] ch, final int start, final int 
length) {
-            // no-op
-        }
-
-        @Override
-        public void endCDATA() {
-            // no-op
-        }
+        private String dtdPublicId;
+        private String dtdSystemId;
+        private boolean inDtd;
 
         @Override
         public void endDTD() {
             inDtd = false;
         }
 
-        @Override
-        public void endEntity(final String name) {
-            // no-op
-        }
-
         @Override
         public InputSource resolveEntity(final String publicId, final String 
systemId) throws SAXException {
             if (inDtd && Objects.equals(publicId, dtdPublicId) && 
Objects.equals(systemId, dtdSystemId)) {
@@ -111,35 +100,88 @@ public InputSource resolveEntity(final String publicId, 
final String systemId) t
             throw new SAXException(forbiddenMessage(publicId, systemId));
         }
 
-        @Override
-        public void startCDATA() {
-            // no-op
-        }
-
         @Override
         public void startDTD(final String name, final String publicId, final 
String systemId) {
             inDtd = true;
             dtdPublicId = publicId;
             dtdSystemId = systemId;
         }
+    }
+
+    /** {@link SAXParser} wrapper whose {@link #getXMLReader()} returns a 
{@link GuardedXMLReader}. */
+    private static final class GuardedSAXParser extends DelegatingSAXParser {
+
+        private final XMLReader guardedReader;
+
+        GuardedSAXParser(final SAXParser delegate, final XMLReader 
guardedReader) {
+            super(delegate);
+            this.guardedReader = guardedReader;
+        }
 
         @Override
-        public void startEntity(final String name) {
-            // no-op
+        public XMLReader getXMLReader() {
+            return guardedReader;
         }
     }
 
+    /** {@link SAXParserFactory} wrapper that produces {@link 
GuardedSAXParser}s. */
+    private static final class GuardedSAXParserFactory extends 
DelegatingSAXParserFactory {
+
+        GuardedSAXParserFactory(final SAXParserFactory delegate) {
+            super(delegate);
+        }
+
+        @Override
+        public SAXParser newSAXParser() throws ParserConfigurationException, 
SAXException {
+            final SAXParser parser = super.newSAXParser();
+            return new GuardedSAXParser(parser, 
configure(parser.getXMLReader()));
+        }
+    }
+
+    /**
+     * {@link XMLReader} wrapper that surfaces ExpatReader's two-feature 
conflict at {@code setFeature} time instead of {@code parse} time.
+     *
+     * <p>Android's {@code ExpatReader.parse()} throws {@link 
SAXNotSupportedException} when {@code namespaces} and {@code 
namespace-prefixes} are both
+     * enabled; on Android 33+ {@code namespaces} defaults to {@code true}, so 
any caller (notably Apache Xalan's identity transformer) that flips
+     * {@code namespace-prefixes} on triggers the throw at parse time. This 
wrapper relocates the throw to the offending {@code setFeature} call, where
+     * Apache Xalan's {@code try { ... } catch (SAXException) { /* We don't 
care. *<!-- -->/ }} swallows it cleanly.</p>
+     */
+    static final class GuardedXMLReader extends DelegatingXMLReader {
+
+        GuardedXMLReader(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) && 
super.getFeature(NAMESPACES_FEATURE)
+                     || NAMESPACES_FEATURE.equals(name)         && 
super.getFeature(NAMESPACE_PREFIXES_FEATURE))) {
+                throw new SAXNotSupportedException(
+                        "ExpatReader cannot have both '" + NAMESPACES_FEATURE
+                      + "' and '" + NAMESPACE_PREFIXES_FEATURE + "' enabled 
simultaneously");
+            }
+            super.setFeature(name, value);
+        }
+    }
     private static final String LEXICAL_HANDLER_PROPERTY = 
"http://xml.org/sax/properties/lexical-handler";;
 
+    private static final String NAMESPACES_FEATURE = 
"http://xml.org/sax/features/namespaces";;
+
+    private static final String NAMESPACE_PREFIXES_FEATURE = 
"http://xml.org/sax/features/namespace-prefixes";;
+
     static DocumentBuilderFactory configure(final DocumentBuilderFactory 
factory) {
         return factory;
     }
 
     static SAXParserFactory configure(final SAXParserFactory factory) {
-        return new HardeningSAXParserFactory(factory, 
AndroidProvider::configure);
+        return new GuardedSAXParserFactory(factory);
     }
 
     static XMLReader configure(final XMLReader reader) {
+        if (reader instanceof GuardedXMLReader) {
+            return reader;
+        }
         final DtdAwareDenyResolver resolver = new DtdAwareDenyResolver();
         reader.setEntityResolver(resolver);
         try {
@@ -147,7 +189,7 @@ static XMLReader configure(final XMLReader reader) {
         } catch (final SAXException e) {
             // ExpatReader recognises the lexical-handler property; if a 
future replacement does not, fall through and lose subset-vs-entity 
discrimination.
         }
-        return reader;
+        return new GuardedXMLReader(reader);
     }
 
     private AndroidProvider() {
diff --git 
a/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java 
b/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java
new file mode 100644
index 0000000..c31ecb1
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/DelegatingSAXParser.java
@@ -0,0 +1,87 @@
+/*
+ * 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 javax.xml.parsers.SAXParser;
+import javax.xml.validation.Schema;
+
+import org.xml.sax.Parser;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+import org.xml.sax.XMLReader;
+
+/**
+ * {@link SAXParser} that forwards every method to a wrapped delegate.
+ *
+ * <p>The non-abstract {@code parse(...)} overloads inherited from {@link 
SAXParser} call {@code this.getXMLReader()} virtually, so a subclass that only
+ * overrides {@link #getXMLReader()} can redirect every parse path through a 
different reader without having to override the overloads.</p>
+ */
+class DelegatingSAXParser extends SAXParser {
+
+    private final SAXParser delegate;
+
+    DelegatingSAXParser(final SAXParser delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    @SuppressWarnings("deprecation")
+    public Parser getParser() throws SAXException {
+        return delegate.getParser();
+    }
+
+    @Override
+    public Object getProperty(final String name) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        return delegate.getProperty(name);
+    }
+
+    @Override
+    public Schema getSchema() {
+        return delegate.getSchema();
+    }
+
+    @Override
+    public XMLReader getXMLReader() throws SAXException {
+        return delegate.getXMLReader();
+    }
+
+    @Override
+    public boolean isNamespaceAware() {
+        return delegate.isNamespaceAware();
+    }
+
+    @Override
+    public boolean isValidating() {
+        return delegate.isValidating();
+    }
+
+    @Override
+    public boolean isXIncludeAware() {
+        return delegate.isXIncludeAware();
+    }
+
+    @Override
+    public void reset() {
+        delegate.reset();
+    }
+
+    @Override
+    public void setProperty(final String name, final Object value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        delegate.setProperty(name, value);
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java 
b/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java
new file mode 100644
index 0000000..5e6e0f2
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/DelegatingXMLReader.java
@@ -0,0 +1,111 @@
+/*
+ * 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 java.io.IOException;
+
+import org.xml.sax.ContentHandler;
+import org.xml.sax.DTDHandler;
+import org.xml.sax.EntityResolver;
+import org.xml.sax.ErrorHandler;
+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;
+
+/**
+ * {@link XMLReader} that forwards every method to a wrapped delegate.
+ */
+class DelegatingXMLReader implements XMLReader {
+
+    private final XMLReader delegate;
+
+    DelegatingXMLReader(final XMLReader delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public ContentHandler getContentHandler() {
+        return delegate.getContentHandler();
+    }
+
+    @Override
+    public DTDHandler getDTDHandler() {
+        return delegate.getDTDHandler();
+    }
+
+    @Override
+    public EntityResolver getEntityResolver() {
+        return delegate.getEntityResolver();
+    }
+
+    @Override
+    public ErrorHandler getErrorHandler() {
+        return delegate.getErrorHandler();
+    }
+
+    @Override
+    public boolean getFeature(final String name) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        return delegate.getFeature(name);
+    }
+
+    @Override
+    public Object getProperty(final String name) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        return delegate.getProperty(name);
+    }
+
+    @Override
+    public void parse(final InputSource input) throws IOException, 
SAXException {
+        delegate.parse(input);
+    }
+
+    @Override
+    public void parse(final String systemId) throws IOException, SAXException {
+        delegate.parse(systemId);
+    }
+
+    @Override
+    public void setContentHandler(final ContentHandler handler) {
+        delegate.setContentHandler(handler);
+    }
+
+    @Override
+    public void setDTDHandler(final DTDHandler handler) {
+        delegate.setDTDHandler(handler);
+    }
+
+    @Override
+    public void setEntityResolver(final EntityResolver resolver) {
+        delegate.setEntityResolver(resolver);
+    }
+
+    @Override
+    public void setErrorHandler(final ErrorHandler handler) {
+        delegate.setErrorHandler(handler);
+    }
+
+    @Override
+    public void setFeature(final String name, final boolean value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        delegate.setFeature(name, value);
+    }
+
+    @Override
+    public void setProperty(final String name, final Object value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        delegate.setProperty(name, value);
+    }
+}
diff --git a/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java
new file mode 100644
index 0000000..2655d78
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/HardeningSchema.java
@@ -0,0 +1,51 @@
+/*
+ * 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 java.util.function.UnaryOperator;
+
+import javax.xml.validation.Schema;
+import javax.xml.validation.Validator;
+import javax.xml.validation.ValidatorHandler;
+
+/**
+ * {@link Schema} wrapper that applies provider-specific decoration to every 
{@link Validator} and {@link ValidatorHandler} the inner Schema produces, then
+ * wraps each {@link Validator} in {@link HardeningValidator} so {@link 
Validator#validate(javax.xml.transform.Source)} runs through
+ * {@link XmlFactories#harden(javax.xml.transform.Source)}.
+ */
+final class HardeningSchema extends Schema {
+
+    private final Schema delegate;
+    private final UnaryOperator<Validator> validatorHardener;
+    private final UnaryOperator<ValidatorHandler> handlerHardener;
+
+    HardeningSchema(final Schema delegate, final UnaryOperator<Validator> 
validatorHardener, final UnaryOperator<ValidatorHandler> handlerHardener) {
+        this.delegate = delegate;
+        this.validatorHardener = validatorHardener;
+        this.handlerHardener = handlerHardener;
+    }
+
+    @Override
+    public Validator newValidator() {
+        return new 
HardeningValidator(validatorHardener.apply(delegate.newValidator()));
+    }
+
+    @Override
+    public ValidatorHandler newValidatorHandler() {
+        return handlerHardener.apply(delegate.newValidatorHandler());
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java
new file mode 100644
index 0000000..eeaa15d
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/HardeningSchemaFactory.java
@@ -0,0 +1,79 @@
+/*
+ * 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 java.util.function.UnaryOperator;
+
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.validation.Schema;
+import javax.xml.validation.SchemaFactory;
+import javax.xml.validation.Validator;
+import javax.xml.validation.ValidatorHandler;
+
+import org.xml.sax.SAXException;
+
+/**
+ * {@link SchemaFactory} wrapper that rewrites every Source-taking entry point 
through {@link XmlFactories#harden(Source)} and applies provider-specific
+ * decoration to every {@link Validator} and {@link ValidatorHandler} that the 
produced {@link Schema} hands out.
+ *
+ * <p>Three layers cooperate:</p>
+ * <ol>
+ *   <li>{@link HardeningSchemaFactory} rewrites the Source on every {@code 
newSchema(Source[])} entry point.</li>
+ *   <li>{@link HardeningSchema} applies the provider-specific hardener to 
every Validator/ValidatorHandler the inner Schema produces (e.g. Xerces 
re-installs
+ *       its {@code LSResourceResolver} and {@code SecurityManager} since it 
does not propagate them through Schema).</li>
+ *   <li>{@link HardeningValidator} rewrites the Source on every {@link 
Validator#validate(Source)} call.</li>
+ * </ol>
+ */
+final class HardeningSchemaFactory extends DelegatingSchemaFactory {
+
+    private final UnaryOperator<Validator> validatorHardener;
+    private final UnaryOperator<ValidatorHandler> handlerHardener;
+
+    HardeningSchemaFactory(final SchemaFactory delegate) {
+        this(delegate, UnaryOperator.identity(), UnaryOperator.identity());
+    }
+
+    HardeningSchemaFactory(final SchemaFactory delegate, final 
UnaryOperator<Validator> validatorHardener,
+            final UnaryOperator<ValidatorHandler> handlerHardener) {
+        super(delegate);
+        this.validatorHardener = validatorHardener;
+        this.handlerHardener = handlerHardener;
+    }
+
+    @Override
+    public Schema newSchema() throws SAXException {
+        return new HardeningSchema(super.newSchema(), validatorHardener, 
handlerHardener);
+    }
+
+    @Override
+    public Schema newSchema(final Source[] schemas) throws SAXException {
+        return new HardeningSchema(super.newSchema(harden(schemas)), 
validatorHardener, handlerHardener);
+    }
+
+    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] = XmlFactories.harden(schemas[i]);
+            }
+        } catch (final TransformerConfigurationException e) {
+            throw new SAXException("Failed to harden schema source", e);
+        }
+        return hardened;
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java
new file mode 100644
index 0000000..09e65f5
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/HardeningTemplates.java
@@ -0,0 +1,52 @@
+/*
+ * 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 javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.URIResolver;
+
+/**
+ * {@link Templates} wrapper whose only purpose is to return a {@link 
HardeningTransformer} from {@link Templates#newTransformer()}, with the 
factory's
+ * compile-time {@link URIResolver} pre-installed.
+ *
+ * <p>Both Apache Xalan 2.7 and stock-JDK XSLTC fail to propagate the 
factory's URIResolver through {@code Templates.newTransformer()}: the produced 
runtime
+ * Transformer has a null URIResolver unless the caller sets one, leaving 
runtime {@code document()} calls unguarded. Snapshotting the resolver at 
compile time
+ * and restoring it onto the runtime Transformer matches the JAXP-conformant 
intuition that the factory's resolver is the default for any Transformer the
+ * factory ultimately produces.</p>
+ */
+final class HardeningTemplates extends DelegatingTemplates {
+
+    /** Compile-time URIResolver snapshot; the underlying impl does not 
propagate the factory's resolver onto Transformers obtained from Templates. */
+    private final URIResolver uriResolver;
+
+    HardeningTemplates(final Templates delegate, final URIResolver 
uriResolver) {
+        super(delegate);
+        this.uriResolver = uriResolver;
+    }
+
+    @Override
+    public Transformer newTransformer() throws 
TransformerConfigurationException {
+        final Transformer transformer = super.newTransformer();
+        if (transformer == null) {
+            return null;
+        }
+        transformer.setURIResolver(uriResolver);
+        return new HardeningTransformer(transformer);
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java
new file mode 100644
index 0000000..12a4a67
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/HardeningTransformer.java
@@ -0,0 +1,43 @@
+/*
+ * 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 javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.TransformerException;
+
+/**
+ * {@link Transformer} wrapper that rewrites the Source on every {@link 
Transformer#transform(Source, Result)} call through
+ * {@link XmlFactories#harden(Source)} before delegating.
+ */
+final class HardeningTransformer extends DelegatingTransformer {
+
+    HardeningTransformer(final Transformer delegate) {
+        super(delegate);
+    }
+
+    @Override
+    public void transform(final Source xmlSource, final Result outputTarget) 
throws TransformerException {
+        try {
+            super.transform(XmlFactories.harden(xmlSource), outputTarget);
+        } catch (final TransformerConfigurationException e) {
+            throw new TransformerException(e);
+        }
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java
new file mode 100644
index 0000000..23e3a57
--- /dev/null
+++ 
b/src/main/java/org/apache/commons/xml/factory/HardeningTransformerFactory.java
@@ -0,0 +1,89 @@
+/*
+ * 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 javax.xml.transform.Source;
+import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
+
+import org.xml.sax.XMLReader;
+
+/**
+ * {@link javax.xml.transform.TransformerFactory} wrapper that rewrites every 
Source-taking entry point through {@link XmlFactories#harden(Source)} before
+ * delegating.
+ *
+ * <p>Used by providers whose underlying TrAX implementation pulls a fresh 
{@code SAXParserFactory.newInstance()} for any Source that is not already a
+ * {@link SAXSource} carrying its own {@link XMLReader}, and only sets {@link 
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING FSP} on the resulting reader.
+ * Wrapping the factory and rewriting the Source upstream guarantees the parse 
runs through an {@link XmlFactories}-hardened reader instead.</p>
+ *
+ * <p>Three layers cooperate:</p>
+ * <ol>
+ *   <li>{@link HardeningTransformerFactory} rewrites the Source on every 
entry point that compiles a stylesheet or transforms a one-shot input.</li>
+ *   <li>{@link HardeningTemplates} returns a {@link HardeningTransformer} 
from {@link Templates#newTransformer()} so runtime source parsing is also 
covered, and
+ *       restores the factory's URIResolver onto the produced Transformer 
(which the underlying impl typically does not propagate through {@code 
Templates}).</li>
+ *   <li>{@link HardeningTransformer} rewrites the Source on every {@link 
Transformer#transform(Source, javax.xml.transform.Result)} call.</li>
+ * </ol>
+ *
+ * <h2>Caveats</h2>
+ * <ul>
+ *   <li>A {@link SAXSource} that carries its own {@link XMLReader} is trusted 
as-is: the caller is expected to supply a hardened reader (via
+ *       {@link XmlFactories#newSAXParserFactory()} or {@link 
XmlFactories#harden(XMLReader)}) in that case.</li>
+ *   <li>{@link TransformerHandler} returned from {@code 
newTransformerHandler} is not wrapped: it processes incoming SAX events instead 
of reading a Source, so
+ *       it has no inner Source-parsing path. A caller who pulls the inner 
{@link Transformer} via {@link TransformerHandler#getTransformer()} bypasses the
+ *       runtime source rewrite.</li>
+ * </ul>
+ */
+final class HardeningTransformerFactory extends DelegatingTransformerFactory {
+
+    HardeningTransformerFactory(final SAXTransformerFactory delegate) {
+        super(delegate);
+    }
+
+    @Override
+    public Source getAssociatedStylesheet(final Source source, final String 
media, final String title, final String charset)
+            throws TransformerConfigurationException {
+        return super.getAssociatedStylesheet(XmlFactories.harden(source), 
media, title, charset);
+    }
+
+    @Override
+    public Templates newTemplates(final Source source) throws 
TransformerConfigurationException {
+        final Templates templates = 
super.newTemplates(XmlFactories.harden(source));
+        return templates == null ? null : new HardeningTemplates(templates, 
getURIResolver());
+    }
+
+    @Override
+    public Transformer newTransformer() throws 
TransformerConfigurationException {
+        // Identity transformer: still parses runtime sources, so wrap it to 
harden Transformer.transform(Source, Result).
+        final Transformer transformer = super.newTransformer();
+        return transformer == null ? null : new 
HardeningTransformer(transformer);
+    }
+
+    @Override
+    public Transformer newTransformer(final Source source) throws 
TransformerConfigurationException {
+        final Transformer transformer = 
super.newTransformer(XmlFactories.harden(source));
+        return transformer == null ? null : new 
HardeningTransformer(transformer);
+    }
+
+    @Override
+    public TransformerHandler newTransformerHandler(final Source source) 
throws TransformerConfigurationException {
+        return super.newTransformerHandler(XmlFactories.harden(source));
+    }
+}
diff --git 
a/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java 
b/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java
new file mode 100644
index 0000000..b970ed9
--- /dev/null
+++ b/src/main/java/org/apache/commons/xml/factory/HardeningValidator.java
@@ -0,0 +1,97 @@
+/*
+ * 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 java.io.IOException;
+
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerConfigurationException;
+import javax.xml.validation.Validator;
+
+import org.w3c.dom.ls.LSResourceResolver;
+import org.xml.sax.ErrorHandler;
+import org.xml.sax.SAXException;
+import org.xml.sax.SAXNotRecognizedException;
+import org.xml.sax.SAXNotSupportedException;
+
+/**
+ * {@link Validator} wrapper that rewrites the Source on every {@link 
Validator#validate(Source)} and {@link Validator#validate(Source, Result)} call 
through
+ * {@link XmlFactories#harden(Source)} before delegating.
+ */
+final class HardeningValidator extends Validator {
+
+    private final Validator delegate;
+
+    HardeningValidator(final Validator delegate) {
+        this.delegate = delegate;
+    }
+
+    @Override
+    public ErrorHandler getErrorHandler() {
+        return delegate.getErrorHandler();
+    }
+
+    @Override
+    public boolean getFeature(final String name) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        return delegate.getFeature(name);
+    }
+
+    @Override
+    public Object getProperty(final String name) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        return delegate.getProperty(name);
+    }
+
+    @Override
+    public LSResourceResolver getResourceResolver() {
+        return delegate.getResourceResolver();
+    }
+
+    @Override
+    public void reset() {
+        delegate.reset();
+    }
+
+    @Override
+    public void setErrorHandler(final ErrorHandler errorHandler) {
+        delegate.setErrorHandler(errorHandler);
+    }
+
+    @Override
+    public void setFeature(final String name, final boolean value) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        delegate.setFeature(name, value);
+    }
+
+    @Override
+    public void setProperty(final String name, final Object object) throws 
SAXNotRecognizedException, SAXNotSupportedException {
+        delegate.setProperty(name, object);
+    }
+
+    @Override
+    public void setResourceResolver(final LSResourceResolver resourceResolver) 
{
+        delegate.setResourceResolver(resourceResolver);
+    }
+
+    @Override
+    public void validate(final Source source, final Result result) throws 
SAXException, IOException {
+        try {
+            delegate.validate(XmlFactories.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/factory/SaxonProvider.java 
b/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java
index da14957..1fdde1c 100644
--- a/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java
+++ b/src/main/java/org/apache/commons/xml/factory/SaxonProvider.java
@@ -58,6 +58,9 @@ private HardenedConfiguration() {
             // URI-resolution layer: empty string disallows every URI scheme. 
Saxon front-ends the existing ResourceResolver with a ProtocolRestrictor; a
             // later setResourceResolver call would cancel this filter, so 
this stays last in the constructor.
             setConfigurationProperty(Feature.ALLOWED_PROTOCOLS, "");
+            // Use the parser below for both style and source:
+            setStyleParserClass("#DEFAULT");
+            setSourceParserClass("#DEFAULT");
         }
 
         /**
diff --git a/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java 
b/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java
index 6d70f5d..e56c46c 100644
--- a/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java
+++ b/src/main/java/org/apache/commons/xml/factory/StockJdkProvider.java
@@ -25,6 +25,7 @@
 import javax.xml.parsers.SAXParserFactory;
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXTransformerFactory;
 import javax.xml.validation.SchemaFactory;
 import javax.xml.xpath.XPathFactory;
 
@@ -79,6 +80,8 @@ static DocumentBuilderFactory configure(final 
DocumentBuilderFactory factory) {
     static SAXParserFactory configure(final SAXParserFactory factory) {
         // Required: enables the JDK XMLSecurityManager limits.
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Useful: namespaces should be recognized by default
+        factory.setNamespaceAware(true);
         // The remaining hardening (limits, ACCESS_EXTERNAL_*) lives in the 
XMLReader configure() because SAXParserFactory has no property API.
         return new HardeningSAXParserFactory(factory, 
StockJdkProvider::configure);
     }
@@ -105,16 +108,16 @@ static XMLInputFactory configure(final XMLInputFactory 
factory) {
     }
 
     static TransformerFactory configure(final TransformerFactory factory) {
-        // Defense-in-depth: pin to the JDK's bundled SAX parser; see 
FEATURE_OVERRIDE_DEFAULT_PARSER.
-        setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false);
-        // Required: enables the JDK XMLSecurityManager limits used by the 
internal SAX parser.
+        // Required: enables XSLTC's runtime evaluator limits (entity 
expansion, attribute count, element/name depth).
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
         // Defense-in-depth: pin to JDK 25 limits so older JDKs do not fall 
back to looser secure values.
         Limits.applyToJdkTransformer(factory);
-        // Defense-in-depth: already FSP-secure defaults, set explicitly so 
they are not relaxed via system property.
+        // Required: XSLTC's compile path (Util.getInputSource) propagates the 
factory's ACCESS_EXTERNAL_DTD onto the SAXSource's reader.
         setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        // Required: Prevents resolution of `xsl:import`, `xsl:include` and 
`document()`.
         setAttribute(factory, XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
-        return factory;
+        // Required: XSLTC's source-document parsing path provisions its own 
SAX reader if the source does not have its own parser.
+        return new HardeningTransformerFactory((SAXTransformerFactory) 
factory);
     }
 
     static XPathFactory configure(final XPathFactory factory) {
@@ -126,16 +129,16 @@ static XPathFactory configure(final XPathFactory factory) 
{
     }
 
     static SchemaFactory configure(final SchemaFactory factory) {
-        // Defense-in-depth: pin to the JDK's bundled SAX parser; see 
FEATURE_OVERRIDE_DEFAULT_PARSER.
-        setFeature(factory, FEATURE_OVERRIDE_DEFAULT_PARSER, false);
-        // Required: enables the JDK XMLSecurityManager limits; propagates to 
Validator and ValidatorHandler.
+        // Required: enables the JDK XMLSecurityManager limits.
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
         // Defense-in-depth: pin to JDK 25 limits so older JDKs do not fall 
back to looser secure values.
         Limits.applyToJdkSchema(factory);
-        // Defense-in-depth: already FSP-secure defaults, set explicitly so 
they are not relaxed via system property.
+        // Required: XMLSchemaLoader propagates this onto its inner SAX 
reader, otherwise it is overrideable by system properties
         setProperty(factory, XMLConstants.ACCESS_EXTERNAL_DTD, "");
+        // Required: gates xs:import/include/redefine fetches and 
xsi:schemaLocation.
         setProperty(factory, XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
-        return factory;
+        // Required: routes every newSchema(Source[]) parse through an 
XmlFactories-hardened reader.
+        return new HardeningSchemaFactory(factory);
     }
 
     private StockJdkProvider() {
diff --git a/src/main/java/org/apache/commons/xml/factory/XalanProvider.java 
b/src/main/java/org/apache/commons/xml/factory/XalanProvider.java
index d666960..0e16034 100644
--- a/src/main/java/org/apache/commons/xml/factory/XalanProvider.java
+++ b/src/main/java/org/apache/commons/xml/factory/XalanProvider.java
@@ -19,24 +19,10 @@
 import static org.apache.commons.xml.factory.JaxpSetters.setFeature;
 
 import javax.xml.XMLConstants;
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.transform.Result;
-import javax.xml.transform.Source;
-import javax.xml.transform.Templates;
-import javax.xml.transform.Transformer;
-import javax.xml.transform.TransformerConfigurationException;
-import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
-import javax.xml.transform.dom.DOMSource;
-import javax.xml.transform.sax.SAXSource;
 import javax.xml.transform.sax.SAXTransformerFactory;
-import javax.xml.transform.sax.TransformerHandler;
 import javax.xml.xpath.XPathFactory;
 
-import org.xml.sax.InputSource;
-import org.xml.sax.SAXException;
-import org.xml.sax.XMLReader;
-
 /**
  * Hardening recipes for the external Apache Xalan distribution (the {@code 
xalan:xalan} artifact).
  *
@@ -50,142 +36,20 @@
  *     <li><strong>{@link DenyAllResolver#URI}</strong>: required. Xalan does 
not implement the JAXP 1.5 {@code ACCESS_EXTERNAL_*} attributes; a deny-all
  *         {@link javax.xml.transform.URIResolver} blocks {@code xsl:include}, 
{@code xsl:import}, {@code xsl:source-document} during stylesheet compilation
  *         and {@code document()}, {@code unparsed-text()}, {@code 
collection()} at runtime.</li>
- *     <li>
- *         <p><strong>{@code Hardening*} wrappers + source rewriting</strong>: 
required. Xalan's source-document parsing path falls back to
- *         {@code SAXParserFactory.newInstance()} whenever the input is not a 
{@link DOMSource} or a {@link SAXSource} carrying its own {@link XMLReader}; it
- *         only sets FSP on the resulting reader. Three wrapper layers are 
needed:</p>
- *         <ol>
- *             <li>{@link HardeningTransformerFactory} rewrites the Source on 
every entry point that compiles a stylesheet.</li>
- *             <li>{@link HardeningTemplates} returns a hardened Transformer 
from {@link Templates#newTransformer()} so runtime source parsing is also
- *                 covered.</li>
- *             <li>{@link HardeningTransformer} rewrites the Source on {@link 
Transformer#transform(Source, Result)}.</li>
- *         </ol>
- *     </li>
+ *     <li><strong>{@link HardeningTransformerFactory} + {@link 
HardeningTemplates} + {@link HardeningTransformer}</strong>: required. Xalan's 
source-document
+ *         parsing path falls back to {@code SAXParserFactory.newInstance()} 
whenever the input is not a {@link javax.xml.transform.dom.DOMSource} or a
+ *         {@link javax.xml.transform.sax.SAXSource} carrying its own {@link 
org.xml.sax.XMLReader}; it only sets FSP on the resulting reader. The wrappers
+ *         rewrite every Source through an {@link XmlFactories}-hardened 
reader so the Xalan internals never get to provision their own.</li>
  * </ul>
  *
  * <h2>Caveats</h2>
  * <ul>
  *     <li>Replacing the {@code URIResolver} on the factory or transformer 
cancels the deny-all hardening for XSLT URI fetches; Xalan exposes no
  *         tamper-resistant equivalent of JAXP 1.5 {@code 
ACCESS_EXTERNAL_STYLESHEET}.</li>
- *     <li>The wrappers trust a {@link SAXSource} that carries its own {@link 
XMLReader}: the caller is expected to supply a hardened reader (via
- *         {@link XmlFactories#newSAXParserFactory()} or {@link 
XmlFactories#harden(XMLReader)}) in that case.</li>
- *     <li>{@link TransformerHandler} returned from {@code 
newTransformerHandler} is not wrapped: it processes incoming SAX events instead 
of reading a Source,
- *         so it has no inner Source-parsing path. A caller who pulls the 
inner {@link Transformer} via {@link TransformerHandler#getTransformer()} 
bypasses
- *         the runtime source rewrite.</li>
  * </ul>
  */
 final class XalanProvider {
 
-    /**
-     * {@link TransformerFactory} wrapper that rewrites every Source-taking 
entry point through {@link #hardenSource(Source)} before delegating.
-     */
-    private static final class HardeningTransformerFactory extends 
DelegatingTransformerFactory {
-
-        HardeningTransformerFactory(final SAXTransformerFactory delegate) {
-            super(delegate);
-        }
-
-        @Override
-        public Source getAssociatedStylesheet(final Source source, final 
String media, final String title, final String charset)
-                throws TransformerConfigurationException {
-            return super.getAssociatedStylesheet(hardenSource(source), media, 
title, charset);
-        }
-
-        @Override
-        public Templates newTemplates(final Source source) throws 
TransformerConfigurationException {
-            final Templates templates = 
super.newTemplates(hardenSource(source));
-            return templates == null ? null : new 
HardeningTemplates(templates);
-        }
-
-        @Override
-        public Transformer newTransformer() throws 
TransformerConfigurationException {
-            // Identity transformer: still parses runtime sources, so wrap it 
to harden Transformer.transform(Source, Result).
-            final Transformer transformer = super.newTransformer();
-            if (transformer == null) {
-                return null;
-            }
-            return new HardeningTransformer(transformer);
-        }
-
-        @Override
-        public Transformer newTransformer(final Source source) throws 
TransformerConfigurationException {
-            final Transformer transformer = 
super.newTransformer(hardenSource(source));
-            if (transformer == null) {
-                return null;
-            }
-            return new HardeningTransformer(transformer);
-        }
-
-        @Override
-        public TransformerHandler newTransformerHandler(final Source source) 
throws TransformerConfigurationException {
-            return super.newTransformerHandler(hardenSource(source));
-        }
-    }
-
-    /**
-     * {@link Templates} wrapper whose only purpose is to return a {@link 
HardeningTransformer} from {@link Templates#newTransformer()}.
-     */
-    private static final class HardeningTemplates extends DelegatingTemplates {
-
-        HardeningTemplates(final Templates delegate) {
-            super(delegate);
-        }
-
-        @Override
-        public Transformer newTransformer() throws 
TransformerConfigurationException {
-            final Transformer transformer = super.newTransformer();
-            if (transformer == null) {
-                return null;
-            }
-            return new HardeningTransformer(transformer);
-        }
-    }
-
-    /**
-     * {@link Transformer} wrapper that rewrites the Source on every {@link 
Transformer#transform(Source, Result)} call.
-     */
-    private static final class HardeningTransformer extends 
DelegatingTransformer {
-
-        HardeningTransformer(final Transformer delegate) {
-            super(delegate);
-        }
-
-        @Override
-        public void transform(final Source xmlSource, final Result 
outputTarget) throws TransformerException {
-            super.transform(hardenSource(xmlSource), outputTarget);
-        }
-    }
-
-    /**
-     * Rewrites a Source to one whose XML reader is hardened by {@link 
XmlFactories}.
-     *
-     * <p>Pass-through cases (the caller is trusted to have arranged 
equivalent hardening already):</p>
-     * <ul>
-     *     <li>{@link DOMSource}: the tree is already built; no parser is 
invoked.</li>
-     *     <li>{@link SAXSource} carrying its own {@link XMLReader}: the 
caller's reader is reused, matching Xalan's own contract.</li>
-     * </ul>
-     *
-     * <p>Otherwise the source is converted via {@link 
SAXSource#sourceToInputSource} and re-wrapped in a {@link SAXSource} backed by 
an
-     * {@link XmlFactories}-hardened reader.</p>
-     */
-    private static Source hardenSource(final Source source) throws 
TransformerConfigurationException {
-        if (source instanceof DOMSource || source instanceof SAXSource && 
((SAXSource) source).getXMLReader() != null) {
-            return source;
-        }
-        final InputSource inputSource = SAXSource.sourceToInputSource(source);
-        if (inputSource == null) {
-            return source;
-        }
-        try {
-            final XMLReader reader = 
XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader();
-            final SAXSource hardened = new SAXSource(reader, inputSource);
-            hardened.setSystemId(source.getSystemId());
-            return hardened;
-        } catch (final ParserConfigurationException | SAXException e) {
-            throw new TransformerConfigurationException("Failed to obtain a 
hardened XMLReader for Xalan source parsing", e);
-        }
-    }
-
     static TransformerFactory configure(final TransformerFactory factory) {
         // Required: enables Xalan's secure-processing mode 
(extension-function block)
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
diff --git a/src/main/java/org/apache/commons/xml/factory/XercesProvider.java 
b/src/main/java/org/apache/commons/xml/factory/XercesProvider.java
index 3c58fa4..8052f48 100644
--- a/src/main/java/org/apache/commons/xml/factory/XercesProvider.java
+++ b/src/main/java/org/apache/commons/xml/factory/XercesProvider.java
@@ -24,15 +24,12 @@
 import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParser;
 import javax.xml.parsers.SAXParserFactory;
-import javax.xml.transform.Source;
 import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 import javax.xml.validation.Validator;
 import javax.xml.validation.ValidatorHandler;
 
-import org.w3c.dom.ls.LSResourceResolver;
 import org.xml.sax.EntityResolver;
-import org.xml.sax.SAXException;
 import org.xml.sax.SAXNotRecognizedException;
 import org.xml.sax.SAXNotSupportedException;
 import org.xml.sax.XMLReader;
@@ -88,73 +85,24 @@ public DocumentBuilder newDocumentBuilder() throws 
ParserConfigurationException
         }
     }
 
-    /**
-     * Hardened Xerces {@link Schema} wrapper.
-     *
-     * <p>Required because Xerces' {@link Schema} does not propagate the 
{@link SchemaFactory}'s resolver or security manager to
-     * {@link Validator} / {@link ValidatorHandler} products, so this wrapper 
re-installs the deny-all resolver (closes the DOCTYPE SYSTEM vector
-     * inside the instance document) and pins the JDK 25 limits on every 
product.</p>
-     */
-    private static final class HardeningSchema extends Schema {
-
-        private final Schema delegate;
-        private final LSResourceResolver resolver;
-
-        HardeningSchema(final Schema delegate, final LSResourceResolver 
resolver) {
-            this.delegate = delegate;
-            this.resolver = resolver;
-        }
-
-        @Override
-        public Validator newValidator() {
-            final Validator validator = delegate.newValidator();
-            try {
-                // Defense-in-depth: pin the validator's security manager to 
JDK 25 limits.
-                
Limits.applyToXerces(validator.getProperty(XERCES_SECURITY_MANAGER_PROPERTY));
-            } catch (final SAXNotRecognizedException | 
SAXNotSupportedException e) {
-                throw new HardeningException("Failed to read Xerces security 
manager from Validator", e);
-            }
-            // Required: re-install the deny-all resolver since 
SchemaFactory-level hardening does not propagate to Validators.
-            validator.setResourceResolver(resolver);
-            return validator;
-        }
-
-        @Override
-        public ValidatorHandler newValidatorHandler() {
-            final ValidatorHandler handler = delegate.newValidatorHandler();
-            try {
-                // Defense-in-depth: pin the handler's security manager to JDK 
25 limits.
-                
Limits.applyToXerces(handler.getProperty(XERCES_SECURITY_MANAGER_PROPERTY));
-            } catch (final SAXNotRecognizedException | 
SAXNotSupportedException e) {
-                throw new HardeningException("Failed to read Xerces security 
manager from ValidatorHandler", e);
-            }
-            // Required: re-install the deny-all resolver since 
SchemaFactory-level hardening does not propagate to ValidatorHandlers.
-            handler.setResourceResolver(resolver);
-            return handler;
+    private static Validator hardenValidator(final Validator validator) {
+        try {
+            
Limits.applyToXerces(validator.getProperty(XERCES_SECURITY_MANAGER_PROPERTY));
+        } catch (final SAXNotRecognizedException | SAXNotSupportedException e) 
{
+            throw new HardeningException("Failed to read Xerces security 
manager from Validator", e);
         }
+        validator.setResourceResolver(DenyAllResolver.LS_RESOURCE);
+        return validator;
     }
 
-    /**
-     * Hardened Xerces {@link SchemaFactory} wrapper.
-     *
-     * <p>Wraps every produced {@link Schema} in {@link HardeningSchema} so 
the SchemaFactory's resolver and security manager are re-installed on each
-     * {@link Validator} / {@link ValidatorHandler}.</p>
-     */
-    private static final class HardeningSchemaFactory extends 
DelegatingSchemaFactory {
-
-        HardeningSchemaFactory(final SchemaFactory delegate) {
-            super(delegate);
-        }
-
-        @Override
-        public Schema newSchema() throws SAXException {
-            return new HardeningSchema(super.newSchema(), 
super.getResourceResolver());
-        }
-
-        @Override
-        public Schema newSchema(final Source[] schemas) throws SAXException {
-            return new HardeningSchema(super.newSchema(schemas), 
super.getResourceResolver());
+    private static ValidatorHandler hardenValidatorHandler(final 
ValidatorHandler handler) {
+        try {
+            
Limits.applyToXerces(handler.getProperty(XERCES_SECURITY_MANAGER_PROPERTY));
+        } catch (final SAXNotRecognizedException | SAXNotSupportedException e) 
{
+            throw new HardeningException("Failed to read Xerces security 
manager from ValidatorHandler", e);
         }
+        handler.setResourceResolver(DenyAllResolver.LS_RESOURCE);
+        return handler;
     }
 
     /**
@@ -189,6 +137,8 @@ static DocumentBuilderFactory configure(final 
DocumentBuilderFactory factory) {
     static SAXParserFactory configure(final SAXParserFactory factory) {
         // Required: enables Xerces' built-in SecurityManager (which is what 
carries the limits).
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
+        // Useful: namespaces should be recognized by default
+        factory.setNamespaceAware(true);
         // The remaining hardening (limits, entity resolver) lives in the 
XMLReader configure() because SAXParserFactory has no property API.
         return new HardeningSAXParserFactory(factory, 
XercesProvider::configure);
     }
@@ -210,18 +160,19 @@ static XMLReader configure(final XMLReader reader) {
     }
 
     static SchemaFactory configure(final SchemaFactory factory) {
-        // Required: enables Xerces' built-in SecurityManager (which is what 
carries the limits).
+        // Required: enables Xerces' built-in SecurityManager.
         setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true);
         try {
-            // Defense-in-depth: pin the factory's security manager to JDK 25 
limits; Xerces' own defaults are looser than even JDK 8.
+            // Required: pins limits to JDK 25 secure values, otherwise 
Xerces' own caps are looser than JDK 8.
             
Limits.applyToXerces(factory.getProperty(XERCES_SECURITY_MANAGER_PROPERTY));
         } catch (final SAXNotRecognizedException | SAXNotSupportedException e) 
{
             throw new HardeningException("Failed to read Xerces security 
manager from SchemaFactory", e);
         }
-        // Required: Xerces does not honour JAXP 1.5 ACCESS_EXTERNAL_*; 
install the deny-all resolver so xs:import/xs:include/xs:redefine cannot fetch.
+        // Required: Xerces ignores ACCESS_EXTERNAL_*; the deny-all resolver 
blocks xs:import/include/redefine fetches.
         factory.setResourceResolver(DenyAllResolver.LS_RESOURCE);
-        // Required: Xerces' Schema does not propagate the SchemaFactory's 
resolver or security manager to Validator/ValidatorHandler; the wrapper 
re-installs.
-        return new HardeningSchemaFactory(factory);
+        // Required: routes every newSchema(Source[]) parse through an 
XmlFactories-hardened reader, and re-installs limits + resolver on each 
Validator and
+        // ValidatorHandler since Xerces' Schema does not propagate factory 
state through.
+        return new HardeningSchemaFactory(factory, 
XercesProvider::hardenValidator, XercesProvider::hardenValidatorHandler);
     }
 
     private XercesProvider() {
diff --git a/src/main/java/org/apache/commons/xml/factory/XmlFactories.java 
b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java
index 00dd1b0..a16fa33 100644
--- a/src/main/java/org/apache/commons/xml/factory/XmlFactories.java
+++ b/src/main/java/org/apache/commons/xml/factory/XmlFactories.java
@@ -18,12 +18,19 @@
 
 import javax.xml.XMLConstants;
 import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
 import javax.xml.parsers.SAXParserFactory;
 import javax.xml.stream.XMLInputFactory;
+import javax.xml.transform.Source;
+import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.sax.SAXSource;
+import javax.xml.transform.stream.StreamSource;
 import javax.xml.validation.SchemaFactory;
 import javax.xml.xpath.XPathFactory;
 
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
 import org.xml.sax.XMLReader;
 
 /**
@@ -142,6 +149,28 @@ private static SchemaFactory dispatch(final SchemaFactory 
factory) {
         }
     }
 
+    /**
+     * Rewrites a {@link Source} so that any SAX parsing it triggers runs 
through an {@link XmlFactories}-hardened {@link XMLReader}.
+     *
+     * <p>Only {@link StreamSource} and {@link SAXSource} without a reader are 
enriched with a hardened reader. Other kinds of sources are returned as-is.</p>
+     *
+     * @param source the source to harden; never {@code null}.
+     * @return a hardened source.
+     * @throws TransformerConfigurationException if a hardened reader cannot 
be obtained.
+     */
+    public static Source harden(final Source source) throws 
TransformerConfigurationException {
+        if (source instanceof StreamSource || source instanceof SAXSource && 
((SAXSource) source).getXMLReader() == null) {
+            try {
+                final XMLReader reader = 
newSAXParserFactory().newSAXParser().getXMLReader();
+                final InputSource inputSource = 
SAXSource.sourceToInputSource(source);
+                return inputSource == null ? source : new SAXSource(reader, 
inputSource);
+            } catch (final ParserConfigurationException | SAXException e) {
+                throw new TransformerConfigurationException("Failed to obtain 
a hardened XMLReader for source parsing", e);
+            }
+        }
+        return source;
+    }
+
     /**
      * Hardens an existing {@link XMLReader}.
      *
@@ -155,6 +184,7 @@ public static XMLReader harden(final XMLReader reader) {
             case 
"com.sun.org.apache.xerces.internal.jaxp.SAXParserImpl$JAXPSAXParser":
                 return StockJdkProvider.configure(reader);
             case "org.apache.harmony.xml.ExpatReader":
+            case 
"org.apache.commons.xml.factory.AndroidProvider$GuardedXMLReader":
                 return AndroidProvider.configure(reader);
             case "org.apache.xerces.jaxp.SAXParserImpl$JAXPSAXParser":
                 return XercesProvider.configure(reader);
diff --git 
a/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java 
b/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java
index 0ce93c6..b130bf0 100644
--- a/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java
+++ b/src/test/java/org/apache/commons/xml/factory/AttackTestSupport.java
@@ -36,14 +36,17 @@
 import javax.xml.stream.XMLInputFactory;
 import javax.xml.stream.XMLStreamException;
 import javax.xml.stream.XMLStreamReader;
+import javax.xml.transform.ErrorListener;
 import javax.xml.transform.Source;
 import javax.xml.transform.Templates;
 import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
 import javax.xml.transform.sax.SAXSource;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
+import javax.xml.validation.Schema;
 import javax.xml.validation.SchemaFactory;
 import javax.xml.validation.Validator;
 
@@ -51,32 +54,38 @@
 import org.junit.jupiter.api.function.ThrowingSupplier;
 import org.w3c.dom.Document;
 import org.w3c.dom.Element;
+import org.xml.sax.ErrorHandler;
 import org.xml.sax.InputSource;
 import org.xml.sax.SAXException;
+import org.xml.sax.SAXParseException;
 import org.xml.sax.XMLReader;
 import org.xml.sax.helpers.DefaultHandler;
 
 /**
  * Shared fixtures for attack tests.
  *
- * <p>The hardened-side helpers come in two flavours, distinguished by their 
suffix:</p>
+ * <p>The hardened-side helpers come in three flavours, distinguished by their 
suffix:</p>
  *
  * <ul>
  *   <li>{@code assert*Blocks(...)} runs the payload through a hardened 
factory from {@link XmlFactories} and asserts the parse throws. Used when the 
hardening
  *       layer is expected to reject the attack outright.</li>
- *   <li>{@code assert*DoesNotLeak(...)} runs the payload through a hardened 
factory and asserts the {@link #LEAKED_MARKER} string does not appear in the
- *       output. The parse may either throw or succeed silently. Used where 
the JAXP API does not give a clean throw hook (SAX content stream, transformer
- *       output) or where the XML spec lets the parser silently skip 
undeclared entities once the external DTD subset has been refused (DOM with an 
external
- *       DTD subset).</li>
+ *   <li>{@code assert*DoesNotLeak(...)} runs the payload through a hardened 
factory and asserts the parse completes without throwing and without producing 
the
+ *       {@link #LEAKED_MARKER} string. Used when the hardening contract 
guarantees the parse succeeds but never resolves the external resource (e.g.
+ *       {@code XERCES_LOAD_EXTERNAL_DTD=false} silently skipping the external 
subset, with the body's undeclared entity reference dropped per XML 1.0
+ *       §4.1).</li>
+ *   <li>{@code assert*BlocksOrDoesNotLeak(...)} accepts either of the 
previous two outcomes. Used where the same hardening contract surfaces 
differently across
+ *       providers (e.g. stock-JDK XSLTC throws via {@code 
ACCESS_EXTERNAL_DTD} while Apache Xalan silently skips because its 
source-rewrite routes parsing
+ *       through a {@code XERCES_LOAD_EXTERNAL_DTD=false} reader).</li>
  * </ul>
  *
  * <p>DOM tests that depend on user-defined entity machinery should gate 
themselves with {@link org.junit.jupiter.api.Assumptions#assumeTrue} on
  * {@link #DOM_RESOLVES_INTERNAL_ENTITIES} so they skip on platforms (such as 
Android with KXmlParser) whose DOM parser does not surface the entity events 
that
  * the strict {@link #assertDomBlocks} assertion expects.</p>
  *
- * <p>The unconfigured-side positive controls keep verbs that match the JAXP 
type they exercise: {@code assert*Resolves(payload)} for direct parsing,
- * {@code assert*Compiles(...)} for {@link SchemaFactory} / {@link 
TransformerFactory} compilation, {@code assertTransformerSucceeds(payload)} for
- * {@code Transformer.transform}, {@code assertValidatorAccepts(payload)} for 
{@code Validator.validate}.</p>
+ * <p>The permissive-side positive controls mirror the hardened-side verbs 
with an {@code assertPermissive*} prefix: {@code assertPermissive*Parses} for 
direct
+ * parsing, {@code assertPermissive*Compiles} for {@link SchemaFactory} / 
{@link TransformerFactory} compilation, {@code 
assertPermissiveTransformerTransforms}
+ * for {@code Transformer.transform}, {@code 
assertPermissiveValidatorValidates} for {@code Validator.validate}. Both sides 
perform the same operation; the
+ * prefix marks which factory hardening level the assertion is set against.</p>
  *
  * <p>Schema and Templates assertions take a {@link Source} so the same helper 
covers both inline-string payloads and resource-backed wrappers; build the
  * source via {@link #streamSource(String)} for a string payload or {@link 
#resourceSource(String)} for a file under {@code src/test/resources/leaked/}. 
The
@@ -88,6 +97,49 @@
  */
 final class AttackTestSupport {
 
+    /**
+     * Strict reporter installed on every hardened factory, parser, validator 
and transformer in the helpers below.
+     *
+     * <p>The hardening layer signals every blocked external fetch and every 
SAX-fatal it could not silently skip via the standard JAXP error channels:
+     * {@link ErrorListener#error(TransformerException)} / {@link 
ErrorListener#fatalError(TransformerException) fatalError} on the TrAX side and
+     * {@link ErrorHandler#error(SAXParseException) error} / {@link 
ErrorHandler#fatalError(SAXParseException) fatalError} on the SAX side. Both 
Apache Xalan's
+     * {@code DefaultErrorHandler(false)} and Saxon's {@code 
StandardErrorListener} are pathologically lenient defaults that swallow these 
events; SAX's
+     * {@link DefaultHandler} treats {@code error} as a no-op. The test 
fixture replaces those defaults with a strict reporter that re-throws on every 
reported
+     * error or fatalError so the helpers can observe the block via the same 
mechanism the spec uses to surface it. Warnings stay silent: they are not 
security
+     * signals.</p>
+     */
+    private static final class StrictReporter implements ErrorListener, 
ErrorHandler {
+
+        @Override
+        public void error(final SAXParseException exception) throws 
SAXException {
+            throw exception;
+        }
+
+        @Override
+        public void error(final TransformerException exception) throws 
TransformerException {
+            throw exception;
+        }
+
+        @Override
+        public void fatalError(final SAXParseException exception) throws 
SAXException {
+            throw exception;
+        }
+
+        @Override
+        public void fatalError(final TransformerException exception) throws 
TransformerException {
+            throw exception;
+        }
+
+        @Override
+        public void warning(final SAXParseException exception) {
+            // not a security signal
+        }
+
+        @Override
+        public void warning(final TransformerException exception) {
+            // not a security signal
+        }
+    }
     /**
      * Trivial W3C XML Schema that validates {@link #xmlBody(String)} output.
      */
@@ -102,7 +154,6 @@ final class AttackTestSupport {
             + "    </xs:complexType>\n"
             + "  </xs:element>\n"
             + "</xs:schema>\n";
-
     /**
      * Benign payload used to probe whether the DOM parser inlines a 
user-defined internal general entity into the tree.
      */
@@ -110,17 +161,14 @@ final class AttackTestSupport {
             "<?xml version=\"1.0\"?>\n"
             + "<!DOCTYPE root [<!ENTITY foo \"bar\">]>\n"
             + "<root>&foo;</root>";
-
     /**
      * Set to {@code true} when the platform's DOM parser supports 
user-defined internal entities.
      *
      * <p>Android's {@code KXmlParser} currently fails this test.</p>
      */
     static final boolean DOM_RESOLVES_INTERNAL_ENTITIES = 
probeDomResolvesInternalEntities();
-
     /** {@code true} when running on Android (Dalvik / ART), {@code false} on 
any standard JVM. Probed once via {@code Class.forName} on {@code 
android.os.Build}. */
     static final boolean IS_ANDROID = probeAndroid();
-
     /**
      * URL form of the JDK's entity-expansion limit property.
      */
@@ -132,6 +180,7 @@ final class AttackTestSupport {
      * presence is the leak signal.</p>
      */
     static final String LEAKED_MARKER = "All your base are belong to us";
+    private static final StrictReporter STRICT_REPORTER = new StrictReporter();
 
     /**
      * Asserts a hardened DOM parse of the payload throws.
@@ -139,25 +188,17 @@ final class AttackTestSupport {
      * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
XmlFactories#newDocumentBuilderFactory()}; only a thrown exception passes.</p>
      */
     static void assertDomBlocks(final String payload) {
-        assertParseFails(() -> 
XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)),
 "DOM", SAXException.class);
+        assertParseFails(() -> 
strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)),
 "DOM", SAXException.class);
     }
 
     /**
-     * Asserts a hardened DOM parse either throws or completes without leaked 
content.
+     * Asserts a hardened DOM parse completes without throwing and without 
leaked content.
      *
-     * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
XmlFactories#newDocumentBuilderFactory()}; XML 1.0 §4.1 permits silent skipping 
of undeclared
-     * entities when an external subset is declared but unread.</p>
+     * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
XmlFactories#newDocumentBuilderFactory()}; use this when the hardening 
guarantee is "the parse
+     * succeeds but never resolves the external resource", e.g. when {@code 
XERCES_LOAD_EXTERNAL_DTD=false} silently skips the external subset.</p>
      */
     static void assertDomDoesNotLeak(final String payload) {
-        assertNoLeak(() -> {
-            final Document doc = 
XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload));
-            if (doc.getDocumentElement() == null) {
-                return "";
-            }
-            // Harmony's DOM returns null from getTextContent() on an element 
whose only children are unresolved EntityReference nodes.
-            final String text = doc.getDocumentElement().getTextContent();
-            return text == null ? "" : text;
-        }, "DOM", SAXException.class);
+        assertNoLeakStrict(() -> domParseAndCaptureText(payload), "DOM");
     }
 
     /**
@@ -166,28 +207,11 @@ static void assertDomDoesNotLeak(final String payload) {
      * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
XmlFactories#newDocumentBuilderFactory()}; positive control for DOCTYPE-only 
payloads.</p>
      */
     static void assertDomParses(final String payload) {
-        assertParseSucceeds(() -> 
XmlFactories.newDocumentBuilderFactory().newDocumentBuilder().parse(inputSource(payload)),
 "DOM");
-    }
-
-    /**
-     * Asserts a permissive DOM parse succeeds.
-     *
-     * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
DocumentBuilderFactory#newInstance()} with FSP off; positive control proving 
the payload is
-     * well-formed.</p>
-     */
-    static void assertDomResolves(final String payload) {
-        assertParseSucceeds(() -> {
-            final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
-            if (!IS_ANDROID) {
-                factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
false);
-            }
-            suppressException(() -> 
factory.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "0"));
-            factory.newDocumentBuilder().parse(inputSource(payload));
-        }, "DOM");
+        assertParseSucceeds(() -> 
strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload)),
 "DOM");
     }
 
     /**
-     * Skeleton for every {@code assert*DoesNotLeak} helper.
+     * Skeleton for every {@code assert*BlocksOrDoesNotLeak} helper.
      *
      * <p>Treats a thrown exception of one of the {@code expected} types as 
"hardening blocked at parse" (acceptable); otherwise asserts the captured output
      * omits {@link #LEAKED_MARKER}. A throw whose type does not match {@code 
expected} fails the test, so unrelated failures (e.g. a {@link 
HardeningException}
@@ -198,7 +222,7 @@ static void assertDomResolves(final String payload) {
      * @param expected    the exception types any of which the hardening layer 
may surface as a clean rejection.
      */
     @SafeVarargs
-    private static void assertNoLeak(final ThrowingSupplier<String> action, 
final String description, final Class<? extends Throwable>... expected) {
+    private static void assertNoLeakOrThrows(final ThrowingSupplier<String> 
action, final String description, final Class<? extends Throwable>... expected) 
{
         final String output;
         try {
             output = action.get();
@@ -212,6 +236,22 @@ private static void assertNoLeak(final 
ThrowingSupplier<String> action, final St
                 "Hardening did not block " + description + "; output contained 
marker '" + LEAKED_MARKER + "'.\nFull output:\n" + output);
     }
 
+    /**
+     * Skeleton for every strict {@code assert*DoesNotLeak} helper.
+     *
+     * <p>Runs the action, lets any thrown exception fail the assertion, and 
asserts that the captured output omits {@link #LEAKED_MARKER}. Use this when the
+     * hardening contract guarantees "parses successfully without resolving 
the external resource"; use {@link #assertNoLeakOrThrows} when the contract is
+     * "either blocks at parse or completes without leaked content".</p>
+     *
+     * @param action      the parse to execute, returning the captured output 
text checked for {@link #LEAKED_MARKER}.
+     * @param description short label naming the JAXP surface under test.
+     */
+    private static void assertNoLeakStrict(final ThrowingSupplier<String> 
action, final String description) {
+        final String output = assertDoesNotThrow(action, "Hardened " + 
description + " parse must not throw");
+        assertFalse(output.contains(LEAKED_MARKER),
+                "Hardening did not block " + description + "; output contained 
marker '" + LEAKED_MARKER + "'.\nFull output:\n" + output);
+    }
+
     /**
      * Asserts the supplied parsing action throws an exception of the {@code 
expected} type.
      *
@@ -241,8 +281,9 @@ static void assertParseFails(final Executable action, final 
String description,
     /**
      * Asserts the supplied action does not throw.
      *
-     * <p>Generic primitive underlying every {@code assert*Parses(...)} / 
{@code assert*Compiles(...)} / {@code assert*Resolves(...)} /
-     * {@code assert*Succeeds(...)} / {@code assertValidatorAccepts(...)} 
helper; exposed for tests that compose a non-standard call.</p>
+     * <p>Generic primitive underlying every {@code assert*Parses(...)} / 
{@code assert*Compiles(...)} / {@code assert*Transforms(...)} /
+     * {@code assert*Validates(...)} helper (and their permissive {@code 
assertPermissive*} counterparts); exposed for tests that compose a non-standard
+     * call.</p>
      *
      * @param action      the parse to execute.
      * @param description short label included in the failure message.
@@ -252,31 +293,20 @@ static void assertParseSucceeds(final Executable action, 
final String descriptio
     }
 
     /**
-     * Asserts a hardened SAX parse of the payload throws.
-     *
-     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; only a thrown exception passes.</p>
-     */
-    static void assertSaxBlocks(final String payload) {
-        assertParseFails(() -> 
parseQuietly(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(), 
payload), "SAX", SAXException.class);
-    }
-
-    /**
-     * Asserts a hardened SAX parse either throws or completes without leaked 
content.
-     *
-     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; XML 1.0 §4.1 permits silent skipping of 
undeclared
-     * entities when an external subset is declared but unread.</p>
-     */
-    static void assertSaxDoesNotLeak(final String payload) {
-        assertNoLeak(() -> 
captureCharacters(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(),
 payload), "SAX", SAXException.class);
-    }
-
-    /**
-     * Asserts a hardened SAX parse succeeds.
+     * Asserts a permissive DOM parse succeeds.
      *
-     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; positive control for DOCTYPE-only 
payloads.</p>
+     * <p>{@link DocumentBuilder#parse(InputSource)} via {@link 
DocumentBuilderFactory#newInstance()} with FSP off; positive control proving 
the payload is
+     * well-formed.</p>
      */
-    static void assertSaxParses(final String payload) {
-        assertParseSucceeds(() -> 
parseQuietly(XmlFactories.newSAXParserFactory().newSAXParser().getXMLReader(), 
payload), "SAX");
+    static void assertPermissiveDomParses(final String payload) {
+        assertParseSucceeds(() -> {
+            final DocumentBuilderFactory factory = 
DocumentBuilderFactory.newInstance();
+            if (!IS_ANDROID) {
+                factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
false);
+            }
+            suppressException(() -> 
factory.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "0"));
+            strictDocumentBuilder(factory).parse(inputSource(payload));
+        }, "DOM");
     }
 
     /**
@@ -285,77 +315,150 @@ static void assertSaxParses(final String payload) {
      * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
SAXParserFactory#newInstance()} with FSP off; positive control proving the 
payload is
      * well-formed.</p>
      */
-    static void assertSaxResolves(final String payload) {
+    static void assertPermissiveSaxParses(final String payload) {
         assertParseSucceeds(() -> {
             final SAXParserFactory factory = SAXParserFactory.newInstance();
             if (!IS_ANDROID) {
                 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
false);
             }
-            final XMLReader reader = factory.newSAXParser().getXMLReader();
+            final XMLReader reader = strictXMLReader(factory);
             suppressException(() -> 
reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
-            parseQuietly(reader, payload);
+            consumeXmlReader(reader, payload);
         }, "SAX");
     }
 
-    /**
-     * Asserts a hardened Schema compilation throws.
-     *
-     * <p>{@link SchemaFactory#newSchema(Source)} via {@link 
XmlFactories#newSchemaFactory()}; only a thrown exception passes.</p>
-     */
-    static void assertSchemaBlocks(final Source xsd) {
-        assertParseFails(() -> XmlFactories.newSchemaFactory().newSchema(xsd), 
"Schema compile", SAXException.class, SecurityException.class);
-    }
-
     /**
      * Asserts a permissive Schema compilation succeeds.
      *
      * <p>{@link SchemaFactory#newSchema(Source)} via {@link 
SchemaFactory#newInstance(String)} with FSP off; positive control proving the 
wrapper is
      * well-formed.</p>
      */
-    static void assertSchemaCompiles(final Source xsd) {
+    static void assertPermissiveSchemaCompiles(final Source xsd) {
         assertParseSucceeds(() -> {
             final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
             if (!IS_ANDROID) {
                 factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
false);
             }
             suppressException(() -> 
factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
-            factory.newSchema(xsd);
+            strictSchema(factory, xsd);
         }, "Schema compile");
     }
 
     /**
-     * Asserts a hardened StAX parse of the payload throws.
+     * Asserts a permissive StAX parse succeeds.
      *
-     * <p>{@link XMLStreamReader} and {@link XMLEventReader} from {@link 
XmlFactories#newXMLInputFactory()}; both flavours are exercised and either must
-     * throw.</p>
+     * <p>{@link XMLStreamReader} from {@link XMLInputFactory#newInstance()} 
with FSP off; positive control proving the payload is well-formed.</p>
      */
-    static void assertStaxBlocks(final String payload) {
-        assertParseFails(() -> 
consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream", 
XMLStreamException.class);
-        assertParseFails(() -> 
consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event", 
XMLStreamException.class);
+    static void assertPermissiveStaxParses(final String payload) {
+        assertParseSucceeds(() -> {
+            final XMLInputFactory factory = XMLInputFactory.newInstance();
+            suppressException(() -> 
factory.setProperty(XMLConstants.FEATURE_SECURE_PROCESSING, false));
+            // URL form of the JDK property; JDK 8's 
XMLSecurityManager.getIndex only matches this form, JDK 11+ accepts both.
+            suppressException(() -> 
factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
+            consumeStreamReader(factory, payload);
+        }, "StAX");
+    }
+
+    /**
+     * Asserts a permissive Templates compilation succeeds.
+     *
+     * <p>{@link TransformerFactory#newTransformer(Source)} via {@link 
TransformerFactory#newInstance()} with FSP off; positive control proving the 
stylesheet
+     * is well-formed.</p>
+     */
+    static void assertPermissiveTemplatesCompiles(final String xslt) {
+        assertParseSucceeds(() -> {
+            final TransformerFactory factory = 
TransformerFactory.newInstance();
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
+            strictTemplates(factory, permissiveSaxSource(xslt));
+        }, "Templates compile");
     }
 
     /**
-     * Asserts a hardened StAX parse succeeds.
+     * Asserts a permissive identity Transformer succeeds.
      *
-     * <p>{@link XMLStreamReader} from {@link 
XmlFactories#newXMLInputFactory()}; positive control for DOCTYPE-only 
payloads.</p>
+     * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} 
via {@link TransformerFactory#newInstance()} with FSP off; positive control 
proving
+     * the payload is well-formed.</p>
      */
-    static void assertStaxParses(final String payload) {
-        assertParseSucceeds(() -> 
consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX");
+    static void assertPermissiveTransformerTransforms(final String payload) {
+        assertParseSucceeds(() -> {
+            final TransformerFactory factory = 
TransformerFactory.newInstance();
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
+            strictTransformer(factory).transform(permissiveSaxSource(payload), 
new StreamResult(new StringWriter()));
+        }, "Transformer");
     }
 
     /**
-     * Asserts a permissive StAX parse succeeds.
+     * Asserts a permissive Validator validation succeeds.
      *
-     * <p>{@link XMLStreamReader} from {@link XMLInputFactory#newInstance()} 
with FSP off; positive control proving the payload is well-formed.</p>
+     * <p>{@link Validator#validate(Source)} on a validator from {@link 
#BENIGN_SCHEMA} compiled via {@link SchemaFactory#newInstance(String)} with FSP 
off;
+     * positive control proving the instance is well-formed.</p>
      */
-    static void assertStaxResolves(final String payload) {
+    static void assertPermissiveValidatorValidates(final String xml) {
         assertParseSucceeds(() -> {
-            final XMLInputFactory factory = XMLInputFactory.newInstance();
-            suppressException(() -> 
factory.setProperty(XMLConstants.FEATURE_SECURE_PROCESSING, false));
-            // URL form of the JDK property; JDK 8's 
XMLSecurityManager.getIndex only matches this form, JDK 11+ accepts both.
+            final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
             suppressException(() -> 
factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
-            consumeStreamReader(factory, payload);
-        }, "StAX");
+            strictValidator(strictSchema(factory, 
streamSource(BENIGN_SCHEMA))).validate(streamSource(xml));
+        }, "Validator");
+    }
+
+    /**
+     * Asserts a hardened SAX parse of the payload throws.
+     *
+     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; only a thrown exception passes.</p>
+     */
+    static void assertSaxBlocks(final String payload) {
+        assertParseFails(() -> 
consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), 
"SAX", SAXException.class);
+    }
+
+    /**
+     * Asserts a hardened SAX parse completes without throwing and without 
leaked content.
+     *
+     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; use this when the hardening guarantee is 
"the parse
+     * succeeds but never resolves the external resource", e.g. when {@code 
XERCES_LOAD_EXTERNAL_DTD=false} silently skips the external subset.</p>
+     */
+    static void assertSaxDoesNotLeak(final String payload) {
+        assertNoLeakStrict(() -> 
captureCharacters(strictXMLReader(XmlFactories.newSAXParserFactory()), 
payload), "SAX");
+    }
+
+    /**
+     * Asserts a hardened SAX parse succeeds.
+     *
+     * <p>{@link XMLReader#parse(InputSource)} on a parser from {@link 
XmlFactories#newSAXParserFactory()}; positive control for DOCTYPE-only 
payloads.</p>
+     */
+    static void assertSaxParses(final String payload) {
+        assertParseSucceeds(() -> 
consumeXmlReader(strictXMLReader(XmlFactories.newSAXParserFactory()), payload), 
"SAX");
+    }
+
+    /**
+     * Asserts a hardened Schema compilation throws.
+     *
+     * <p>{@link SchemaFactory#newSchema(Source)} via {@link 
XmlFactories#newSchemaFactory()}; only a thrown exception passes.</p>
+     */
+    static void assertSchemaBlocks(final Source xsd) {
+        assertParseFails(() -> strictSchema(XmlFactories.newSchemaFactory(), 
xsd), "Schema compile", SAXException.class, SecurityException.class);
+    }
+
+    /**
+     * Asserts a hardened Schema compilation completes without throwing.
+     *
+     * <p>{@link SchemaFactory#newSchema(Source)} via {@link 
XmlFactories#newSchemaFactory()}; use this when the hardening contract 
guarantees the compile
+     * succeeds but never resolves the external resource (e.g. {@code 
XERCES_LOAD_EXTERNAL_DTD=false} silently skipping the external subset, with the 
body's
+     * undeclared entity reference dropped per XML 1.0 §4.1).</p>
+     */
+    static void assertSchemaDoesNotLeak(final Source xsd) {
+        assertParseSucceeds(() -> 
strictSchema(XmlFactories.newSchemaFactory(), xsd), "Schema compile");
+    }
+
+    /**
+     * Asserts a hardened StAX parse of the payload throws.
+     *
+     * <p>{@link XMLStreamReader} and {@link XMLEventReader} from {@link 
XmlFactories#newXMLInputFactory()}; both flavours are exercised and either must
+     * throw.</p>
+     */
+    static void assertStaxBlocks(final String payload) {
+        assertParseFails(() -> 
consumeStreamReader(XmlFactories.newXMLInputFactory(), payload), "StAX stream", 
XMLStreamException.class);
+        assertParseFails(() -> 
consumeEventReader(XmlFactories.newXMLInputFactory(), payload), "StAX event", 
XMLStreamException.class);
     }
 
     /**
@@ -366,45 +469,33 @@ static void assertStaxResolves(final String payload) {
      */
     static void assertTemplatesBlocks(final Source xslt) {
         assertParseFails(() -> {
-            final Templates templates = 
XmlFactories.newTransformerFactory().newTemplates(xslt);
-            // Xalan return `null` if the template fails
+            final Templates templates = 
strictTemplates(XmlFactories.newTransformerFactory(), xslt);
+            // Xalan returns `null` if the template fails
             if (templates == null) {
                 throw new TransformerException("Transformer factory returned 
null");
             }
-            templates.newTransformer().transform(streamSource("<root/>"), new 
StreamResult(new StringWriter()));
+            strictTransformer(templates).transform(streamSource("<root/>"), 
new StreamResult(new StringWriter()));
         }, "Templates", TransformerException.class);
     }
 
     /**
-     * Asserts a permissive Templates compilation succeeds.
+     * Asserts a hardened Templates compile-and-transform either throws or 
completes without leaked content.
      *
-     * <p>{@link TransformerFactory#newTransformer(Source)} via {@link 
TransformerFactory#newInstance()} with FSP off; positive control proving the 
stylesheet
-     * is well-formed.</p>
+     * <p>{@link TransformerFactory#newTemplates(Source)} via {@link 
XmlFactories#newTransformerFactory()} followed by transform; Saxon's TrAX path 
may swallow
+     * the entity-expansion error and produce empty output.</p>
      */
-    static void assertTemplatesCompiles(final String xslt) {
-        assertParseSucceeds(() -> {
-            final TransformerFactory factory = 
TransformerFactory.newInstance();
-            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
-            factory.newTransformer(permissiveSaxSource(xslt));
-        }, "Templates compile");
+    static void assertTemplatesBlocksOrDoesNotLeak(final Source xslt) {
+        assertNoLeakOrThrows(() -> templatesCompileAndTransform(xslt), 
"Templates", TransformerException.class);
     }
 
     /**
-     * Asserts a hardened Templates compile-and-transform either throws or 
completes without leaked content.
+     * Asserts a hardened Templates compile-and-transform completes without 
throwing and without leaked content.
      *
-     * <p>{@link TransformerFactory#newTemplates(Source)} via {@link 
XmlFactories#newTransformerFactory()} followed by transform; Saxon's TrAX path 
may swallow
-     * the entity-expansion error and produce empty output.</p>
+     * <p>{@link TransformerFactory#newTemplates(Source)} via {@link 
XmlFactories#newTransformerFactory()} followed by transform; use this when the 
hardening
+     * contract guarantees the compile and transform succeed but never resolve 
the external resource.</p>
      */
     static void assertTemplatesDoesNotLeak(final Source xslt) {
-        assertNoLeak(() -> {
-            final StringWriter sink = new StringWriter();
-            final Templates templates = 
XmlFactories.newTransformerFactory().newTemplates(xslt);
-            // Xalan return `null` if the template fails
-            if (templates != null) {
-                templates.newTransformer().transform(streamSource("<root/>"), 
new StreamResult(sink));
-            }
-            return sink.toString();
-        }, "Templates", TransformerException.class);
+        assertNoLeakStrict(() -> templatesCompileAndTransform(xslt), 
"Templates");
     }
 
     /**
@@ -415,7 +506,7 @@ static void assertTemplatesDoesNotLeak(final Source xslt) {
      */
     static void assertTransformerBlocks(final String payload) {
         assertParseFails(
-                () -> 
XmlFactories.newTransformerFactory().newTransformer().transform(streamSource(payload),
 new StreamResult(new StringWriter())),
+                () -> 
strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload),
 new StreamResult(new StringWriter())),
                 "Transformer", TransformerException.class);
     }
 
@@ -425,41 +516,18 @@ static void assertTransformerBlocks(final String payload) 
{
      * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} 
via {@link XmlFactories#newTransformerFactory()}; Saxon's TrAX path may swallow
      * internal errors and produce empty output.</p>
      */
-    static void assertTransformerDoesNotLeak(final String payload) {
-        assertNoLeak(() -> {
-            final StringWriter sink = new StringWriter();
-            
XmlFactories.newTransformerFactory().newTransformer().transform(streamSource(payload),
 new StreamResult(sink));
-            return sink.toString();
-        }, "Transformer", TransformerException.class);
+    static void assertTransformerBlocksOrDoesNotLeak(final String payload) {
+        assertNoLeakOrThrows(() -> identityTransformAndCapture(payload), 
"Transformer", TransformerException.class);
     }
 
     /**
-     * Asserts a permissive identity Transformer succeeds.
+     * Asserts a hardened identity Transformer completes without throwing and 
without leaked content.
      *
-     * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} 
via {@link TransformerFactory#newInstance()} with FSP off; positive control 
proving
-     * the payload is well-formed.</p>
+     * <p>{@link Transformer#transform(Source, javax.xml.transform.Result)} 
via {@link XmlFactories#newTransformerFactory()}; use this when the hardening
+     * contract guarantees the transform succeeds but never resolves the 
external resource.</p>
      */
-    static void assertTransformerSucceeds(final String payload) {
-        assertParseSucceeds(() -> {
-            final TransformerFactory factory = 
TransformerFactory.newInstance();
-            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
-            factory.newTransformer().transform(permissiveSaxSource(payload), 
new StreamResult(new StringWriter()));
-        }, "Transformer");
-    }
-
-    /**
-     * Asserts a permissive Validator validation succeeds.
-     *
-     * <p>{@link Validator#validate(Source)} on a validator from {@link 
#BENIGN_SCHEMA} compiled via {@link SchemaFactory#newInstance(String)} with FSP 
off;
-     * positive control proving the instance is well-formed.</p>
-     */
-    static void assertValidatorAccepts(final String xml) {
-        assertParseSucceeds(() -> {
-            final SchemaFactory factory = 
SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
-            factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, false);
-            suppressException(() -> 
factory.setProperty(JDK_ENTITY_EXPANSION_LIMIT, "0"));
-            
factory.newSchema(streamSource(BENIGN_SCHEMA)).newValidator().validate(streamSource(xml));
-        }, "Validator");
+    static void assertTransformerDoesNotLeak(final String payload) {
+        assertNoLeakStrict(() -> identityTransformAndCapture(payload), 
"Transformer");
     }
 
     /**
@@ -470,27 +538,39 @@ static void assertValidatorAccepts(final String xml) {
      */
     static void assertValidatorBlocks(final String xml) {
         assertParseFails(
-                () -> 
XmlFactories.newSchemaFactory().newSchema(streamSource(BENIGN_SCHEMA)).newValidator().validate(streamSource(xml)),
+                () -> 
strictValidator(strictSchema(XmlFactories.newSchemaFactory(), 
streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)),
                 "Validator", SAXException.class, SecurityException.class);
     }
 
+    /**
+     * Asserts a hardened Validator validation completes without throwing.
+     *
+     * <p>{@link Validator#validate(Source)} on a validator from {@link 
#BENIGN_SCHEMA} compiled via {@link XmlFactories#newSchemaFactory()}; use this 
when the
+     * hardening contract guarantees the validate succeeds but never resolves 
the external resource.</p>
+     */
+    static void assertValidatorDoesNotLeak(final String xml) {
+        assertParseSucceeds(
+                () -> 
strictValidator(strictSchema(XmlFactories.newSchemaFactory(), 
streamSource(BENIGN_SCHEMA))).validate(streamSource(xml)),
+                "Validator");
+    }
+
     /**
      * Asserts a hardened-in-place XMLReader parse of the payload throws.
      *
      * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link XmlFactories#harden(XMLReader)}; only a thrown exception passes.</p>
      */
     static void assertXmlReaderBlocks(final String payload) {
-        assertParseFails(() -> parseQuietly(rawHardenedReader(), payload), 
"XMLReader", SAXException.class);
+        assertParseFails(() -> consumeXmlReader(rawHardenedReader(), payload), 
"XMLReader", SAXException.class);
     }
 
     /**
-     * Asserts a hardened-in-place XMLReader parse either throws or completes 
without leaked content.
+     * 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 XmlFactories#harden(XMLReader)}; XML 1.0 §4.1 permits silent skipping of
-     * undeclared entities when an external subset is declared but unread.</p>
+     * <p>{@link XMLReader#parse(InputSource)} on a raw reader hardened via 
{@link XmlFactories#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) {
-        assertNoLeak(() -> captureCharacters(rawHardenedReader(), payload), 
"XMLReader", SAXException.class);
+        assertNoLeakStrict(() -> captureCharacters(rawHardenedReader(), 
payload), "XMLReader");
     }
 
     /**
@@ -500,7 +580,7 @@ static void assertXmlReaderDoesNotLeak(final String 
payload) {
      * payloads.</p>
      */
     static void assertXmlReaderParses(final String payload) {
-        assertParseSucceeds(() -> parseQuietly(rawHardenedReader(), payload), 
"XMLReader");
+        assertParseSucceeds(() -> consumeXmlReader(rawHardenedReader(), 
payload), "XMLReader");
     }
 
     /**
@@ -522,8 +602,7 @@ public void characters(final char[] ch, final int start, 
final int length) {
                 text.append(ch, start, length);
             }
         });
-        reader.setErrorHandler(new DefaultHandler());
-        reader.parse(inputSource(payload));
+        strictXMLReader(reader).parse(inputSource(payload));
         return text.toString();
     }
 
@@ -551,27 +630,42 @@ private static void consumeStreamReader(final 
XMLInputFactory factory, final Str
         }
     }
 
-    /** Builds an {@link InputSource} backed by a {@link StringReader} over 
the payload. */
-    static InputSource inputSource(final String xml) {
-        return new InputSource(new StringReader(xml));
-    }
-
     /** Parses the payload through the supplied reader, discarding events; 
used by the SAX-based {@code Parses} / {@code Blocks} helpers. */
-    private static void parseQuietly(final XMLReader reader, final String 
payload) throws Exception {
+    private static void consumeXmlReader(final XMLReader reader, final String 
payload) throws Exception {
         reader.setContentHandler(new DefaultHandler());
-        reader.setErrorHandler(new DefaultHandler());
-        reader.parse(inputSource(payload));
+        strictXMLReader(reader).parse(inputSource(payload));
     }
 
-    /** Builds a {@link SAXSource} wrapping the payload, parsed by a 
deliberately permissive SAX parser; used by the unconfigured-side TrAX 
controls. */
-    private static SAXSource permissiveSaxSource(final String xml) throws 
ParserConfigurationException, org.xml.sax.SAXException {
-        final SAXParserFactory parserFactory = SAXParserFactory.newInstance();
-        if (!IS_ANDROID) {
-            parserFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, 
false);
+    private static String domParseAndCaptureText(final String payload) throws 
Exception {
+        final Document doc = 
strictDocumentBuilder(XmlFactories.newDocumentBuilderFactory()).parse(inputSource(payload));
+        if (doc.getDocumentElement() == null) {
+            return "";
         }
-        final XMLReader reader = parserFactory.newSAXParser().getXMLReader();
+        // Harmony's DOM returns null from getTextContent() on an element 
whose only children are unresolved EntityReference nodes.
+        final String text = doc.getDocumentElement().getTextContent();
+        return text == null ? "" : text;
+    }
+
+    private static String identityTransformAndCapture(final String payload) 
throws TransformerException {
+        final StringWriter sink = new StringWriter();
+        
strictTransformer(XmlFactories.newTransformerFactory()).transform(streamSource(payload),
 new StreamResult(sink));
+        return sink.toString();
+    }
+
+    /** Builds an {@link InputSource} backed by a {@link StringReader} over 
the payload. */
+    static InputSource inputSource(final String xml) {
+        return new InputSource(new StringReader(xml));
+    }
+
+    /**
+     * Builds a {@link SAXSource} wrapping the payload, without an explicit 
parser; used by the unconfigured-side TrAX controls.
+     */
+    private static SAXSource permissiveSaxSource(final String xml) {
+        final SAXParserFactory factory = SAXParserFactory.newInstance();
+        factory.setNamespaceAware(true);
+        final XMLReader reader = strictXMLReader(factory);
         suppressException(() -> reader.setProperty(JDK_ENTITY_EXPANSION_LIMIT, 
"0"));
-        return new SAXSource(reader, new InputSource(new StringReader(xml)));
+        return new SAXSource(IS_ANDROID ? new 
AndroidProvider.GuardedXMLReader(reader) : reader, new InputSource(new 
StringReader(xml)));
     }
 
     private static boolean probeAndroid() {
@@ -621,7 +715,81 @@ static URL resourceUrl(final String name) {
 
     /** Builds a {@link StreamSource} backed by a {@link StringReader} over 
the payload. */
     static StreamSource streamSource(final String xml) {
-        return new StreamSource(new StringReader(xml));
+        StreamSource streamSource = new StreamSource(new StringReader(xml));
+        streamSource.setSystemId("test:fixture");
+        return streamSource;
+    }
+
+    /**
+     * Builds a {@link DocumentBuilder} from {@code factory} with {@link 
#STRICT_REPORTER} installed as its error handler.
+     */
+    private static DocumentBuilder strictDocumentBuilder(final 
DocumentBuilderFactory factory) throws ParserConfigurationException {
+        final DocumentBuilder builder = factory.newDocumentBuilder();
+        builder.setErrorHandler(STRICT_REPORTER);
+        return builder;
+    }
+
+    /**
+     * Compiles {@code xsds} into a {@link Schema} using {@code factory}, with 
{@link #STRICT_REPORTER} installed on the factory before compile.
+     */
+    private static Schema strictSchema(final SchemaFactory factory, final 
Source... xsds) throws SAXException {
+        factory.setErrorHandler(STRICT_REPORTER);
+        return factory.newSchema(xsds);
+    }
+
+    /**
+     * Compiles {@code xslt} into a {@link Templates} using {@code factory}, 
with {@link #STRICT_REPORTER} installed on the factory before compile.
+     */
+    private static Templates strictTemplates(final TransformerFactory factory, 
final Source xslt) throws TransformerConfigurationException {
+        factory.setErrorListener(STRICT_REPORTER);
+        return factory.newTemplates(xslt);
+    }
+
+    /**
+     * Builds an identity {@link Transformer} from {@code factory} with {@link 
#STRICT_REPORTER} installed on both the factory and the resulting transformer.
+     */
+    private static Transformer strictTransformer(final TransformerFactory 
factory) throws TransformerConfigurationException {
+        factory.setErrorListener(STRICT_REPORTER);
+        final Transformer transformer = factory.newTransformer();
+        transformer.setErrorListener(STRICT_REPORTER);
+        return transformer;
+    }
+
+    /**
+     * Builds a {@link Transformer} from {@code templates} with {@link 
#STRICT_REPORTER} installed as its error listener.
+     */
+    private static Transformer strictTransformer(final Templates templates) 
throws TransformerConfigurationException {
+        final Transformer transformer = templates.newTransformer();
+        transformer.setErrorListener(STRICT_REPORTER);
+        return transformer;
+    }
+
+    /**
+     * Builds a {@link Validator} from {@code schema} with {@link 
#STRICT_REPORTER} installed as its error handler.
+     */
+    private static Validator strictValidator(final Schema schema) {
+        final Validator validator = schema.newValidator();
+        validator.setErrorHandler(STRICT_REPORTER);
+        return validator;
+    }
+
+    /**
+     * Builds an {@link XMLReader} from {@code factory} with {@link 
#STRICT_REPORTER} installed as its error handler.
+     */
+    private static XMLReader strictXMLReader(final SAXParserFactory factory) {
+        try {
+            return strictXMLReader(factory.newSAXParser().getXMLReader());
+        } catch (ParserConfigurationException | SAXException e) {
+            throw new AssertionError("Failed to create permissive XMLReader", 
e);
+        }
+    }
+
+    /**
+     * Installs {@link #STRICT_REPORTER} as the error handler on {@code 
reader} and returns it; for raw-reader paths.
+     */
+    private static XMLReader strictXMLReader(final XMLReader reader) {
+        reader.setErrorHandler(STRICT_REPORTER);
+        return reader;
     }
 
     /** Runs the action and silently swallows any thrown exception; used to 
apply best-effort permissive-side flags that may not be supported. */
@@ -633,6 +801,16 @@ private static void suppressException(final Executable 
action) {
         }
     }
 
+    private static String templatesCompileAndTransform(final Source xslt) 
throws TransformerException {
+        final StringWriter sink = new StringWriter();
+        final Templates templates = 
strictTemplates(XmlFactories.newTransformerFactory(), xslt);
+        // Xalan returns `null` if the template fails
+        if (templates != null) {
+            strictTransformer(templates).transform(streamSource("<root/>"), 
new StreamResult(sink));
+        }
+        return sink.toString();
+    }
+
     /** XML body wrapping the supplied text in a benign {@code <root>/<child>} 
element pair, validated by {@link #BENIGN_SCHEMA}. */
     static String xmlBody(final String text) {
         return "<root><child>" + text + "</child></root>";
diff --git 
a/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java 
b/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java
index accae35..23ae725 100644
--- a/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/BillionLaughsTest.java
@@ -33,8 +33,7 @@
  *       property name, so it parses the ~4 KB of expanded {@code "A"} text 
and finishes immediately.</li>
  *   <li>The <strong>large</strong> fixture nests seven levels of 10x 
expansion ({@code lol1} through {@code lol7}), so resolving {@code &lol7;} 
produces
  *       {@code 10^7 = 10 000 000} leaf expansions, ~10 MB of {@code "A"} 
text, and an amplification factor in the tens of thousands. Sized to trip
- *       libexpat &gt;= 2.4's built-in billion-laughs check (8 MiB activation 
threshold and 100x amplification factor); used by the hardened DOM/SAX/XmlReader
- *       tests on Android, where the JDK count limit is unavailable and only 
libexpat's native check stands between the parser and the expansion.</li>
+ *       libexpat &gt;= 2.4's built-in billion-laughs check (8 MiB activation 
threshold and 100x amplification factor).</li>
  * </ul>
  *
  * <p>Why a single character {@code "A"}: XSLTC compiles a stylesheet's 
expanded text into a JVM string constant, which is capped at 65535 bytes. A 
larger
@@ -45,30 +44,16 @@
  * <p>Which fixture each test uses:</p>
  *
  * <ul>
- *   <li>{@code hardenedSaxBlocks}, {@code hardenedXmlReaderBlocks}: large 
fixture on Android (libexpat is the only defence), medium fixture on JDK
- *       (entity-expansion count limit is sufficient). Selected by {@link 
AttackTestSupport#IS_ANDROID}.</li>
- *   <li>{@code hardenedDomBlocks} and {@code unconfiguredDomResolves}: gated 
on {@link AttackTestSupport#DOM_RESOLVES_INTERNAL_ENTITIES}. They run with the
- *       medium fixture on platforms whose DOM parser resolves user-defined 
entities (every JDK), and skip on platforms where it does not (Android with
- *       KXmlParser, where custom internal entities become unresolved {@code 
EntityReference} nodes and amplification cannot grow). When the probe lights up
- *       on a future Android the assertions will start running again without 
further changes.</li>
- *   <li>Every other test: medium fixture. {@code unconfigured*} positive 
controls cannot use the large fixture because libexpat's protection cannot be
- *       disabled from Java. {@code Templates} and {@code Transformer} tests 
cannot use it because of the XSLTC 60 KB cap. {@code Schema}, {@code StAX} and
- *       {@code Validator} hardened tests do not run on Android (no harmony 
{@code SchemaFactory} or {@code XMLInputFactory}), so the JDK count limit is the
- *       only relevant defence for them and the medium fixture is enough.</li>
- * </ul>
- *
- * <p>Each parser type is exercised twice as a pair (unconfigured factory, 
expected to parse; hardened factory, expected to throw):</p>
- *
- * <ul>
- *   <li>The {@code hardened*} side runs the payload through {@link 
org.apache.commons.xml.factory.XmlFactories} and asserts the parse throws. 
Hardening blocks
- *       at whichever layer fires first (DOCTYPE-disallow on DOM/SAX, FSP plus 
propagated entity-expansion limits elsewhere).</li>
- *   <li>The {@code unconfigured*} side runs the payload through a default 
factory with {@code FEATURE_SECURE_PROCESSING=false} and asserts the parse 
succeeds.
- *       Disabling FSP turns off the JDK's entity-expansion limit, which is 
the only safety net stopping a default factory from materialising the expansion.
- *       The {@code unconfigured*} name is slightly euphemistic here: the 
factory is actively configured to be permissive, not "left alone".</li>
+ *   <li>{@code hardened*} tests pull from {@code hardened*Payload} helpers: 
large fixture on Android (libexpat is the only defence), medium fixture on JDK
+ *       (entity-expansion count limit is sufficient).</li>
+ *   <li>{@code unconfigured*} positive controls pull from {@code 
medium*Payload} helpers regardless of platform: libexpat's billion-laughs check 
cannot be
+ *       disabled from Java, so a permissive parse of the large fixture would 
still trip on Android.</li>
  * </ul>
  */
 class BillionLaughsTest {
 
+    private static final String LARGE_CONTENT = "&lol7;";
+
     private static final String LARGE_DTD =
             "  <!ENTITY lol \"A\">\n"
             + "  <!ENTITY lol1 
\"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n"
@@ -79,7 +64,7 @@ class BillionLaughsTest {
             + "  <!ENTITY lol6 
\"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n"
             + "  <!ENTITY lol7 
\"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n";
 
-    private static final String LARGE_CONTENT = "&lol7;";
+    private static final String MEDIUM_CONTENT = "&lol3;";
 
     private static final String MEDIUM_DTD =
             "  <!ENTITY lol \"A\">\n"
@@ -87,30 +72,89 @@ class BillionLaughsTest {
             + "  <!ENTITY lol2 
\"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n"
             + "  <!ENTITY lol3 
\"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n";
 
-    private static final String MEDIUM_CONTENT = "&lol3;";
-
     /**
-     * Hardened-side payload for DOM/SAX/XmlReader: large on Android (libexpat 
amplification check), medium on JDK (entity-expansion count limit).
+     * Hardened-side payload for DOM/SAX/XmlReader
+     *
+     * <ul>
+     *     <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li>
+     *     <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 
2500}.</li>
+     * </ul>
      */
     private static String hardenedXmlPayload() {
-        return AttackTestSupport.IS_ANDROID ? largeXmlPayload() : 
mediumXmlPayload();
+        return AttackTestSupport.IS_ANDROID
+                ? withDoctype("root", LARGE_DTD, 
AttackTestSupport.xmlBody(LARGE_CONTENT))
+                : mediumXmlPayload();
     }
 
     /**
-     * ~10 MB of expanded {@code "A"}; trips libexpat &gt;= 2.4's 
billion-laughs check. Not usable for Templates/Transformer (XSLTC 60 KB cap) or 
for any
-     * unconfigured positive control.
+     * Hardened-side XSD payload
+     *
+     * <ul>
+     *     <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li>
+     *     <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 
2500}.</li>
+     * </ul>
      */
-    private static String largeXmlPayload() {
-        return withDoctype("root", LARGE_DTD, 
AttackTestSupport.xmlBody(LARGE_CONTENT));
+    private static String hardenedXsdPayload() {
+        return AttackTestSupport.IS_ANDROID
+                ? withDoctype("xs:schema", LARGE_DTD, 
AttackTestSupport.xsdBody(LARGE_CONTENT))
+                : mediumXsdPayload();
     }
 
     /**
-     * ~4 KB of expanded {@code "A"}; trips JDK 25's {@code 
entityExpansionLimit = 2500}. Universal payload that stays under XSLTC's 60 KB 
constant-pool cap.
+     * Hardened-side XSLT payload
+     *
+     * <ul>
+     *     <li>~10 MiB on Android to trip libexpats 8 MiB limit, and</li>
+     *     <li>~4 KiB on JDK to trip JDK 25's {@code entityExpansionLimit = 
2500}, and</li>
+     *     <li>stay under XSLTC's 60 KB constant-pool cap, and</li>
+     * </ul>
+     */
+    private static String hardenedXsltPayload() {
+        return AttackTestSupport.IS_ANDROID
+                ? withDoctype("xsl:stylesheet", LARGE_DTD, 
AttackTestSupport.xsltBody(LARGE_CONTENT))
+                : mediumXsltPayload();
+    }
+
+    /**
+     * Unconfigured-side payload for DOM/SAX/XmlReader
+     *
+     * <p>~4 KB of expanded {@code "A"}, which:</p>
+     * <ul>
+     *     <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li>
+     *     <li>does not trip libexpat's immutable 8 MiB limit.</li>
+     * </ul>
      */
     private static String mediumXmlPayload() {
         return withDoctype("root", MEDIUM_DTD, 
AttackTestSupport.xmlBody(MEDIUM_CONTENT));
     }
 
+    /**
+     * Unconfigured-side XSD payload
+     *
+     * <p>~4 KB of expanded {@code "A"}, which:</p>
+     * <ul>
+     *     <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li>
+     *     <li>does not trip libexpat's immutable 8 MiB limit.</li>
+     * </ul>
+     */
+    private static String mediumXsdPayload() {
+        return withDoctype("xs:schema", MEDIUM_DTD, 
AttackTestSupport.xsdBody(MEDIUM_CONTENT));
+    }
+
+    /**
+     * Unconfigured-side XSLT payload
+     *
+     * <p>~4 KB of expanded {@code "A"}, which:</p>
+     * <ul>
+     *     <li>trips JDK 25's {@code entityExpansionLimit = 2500}, but</li>
+     *     <li>stays under XSLTC's 60 KB constant-pool cap, and</li>
+     *     <li>does not trip libexpat's immutable 8 MiB limit.</li>
+     * </ul>
+     */
+    private static String mediumXsltPayload() {
+        return withDoctype("xsl:stylesheet", MEDIUM_DTD, 
AttackTestSupport.xsltBody(MEDIUM_CONTENT));
+    }
+
     private static String withDoctype(final String rootQName, final String 
dtd, final String body) {
         return "<?xml version=\"1.0\"?>\n"
                 + "<!DOCTYPE " + rootQName + " [\n"
@@ -119,14 +163,6 @@ private static String withDoctype(final String rootQName, 
final String dtd, fina
                 + body + "\n";
     }
 
-    private static String xsdPayload() {
-        return withDoctype("xs:schema", MEDIUM_DTD, 
AttackTestSupport.xsdBody(MEDIUM_CONTENT));
-    }
-
-    private static String xsltPayload() {
-        return withDoctype("xsl:stylesheet", MEDIUM_DTD, 
AttackTestSupport.xsltBody(MEDIUM_CONTENT));
-    }
-
     @Test
     @Tag("dom")
     void hardenedDomBlocks() {
@@ -144,31 +180,31 @@ void hardenedSaxBlocks() {
     @Test
     @Tag("schema")
     void hardenedSchemaBlocks() {
-        
AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload()));
+        
AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(hardenedXsdPayload()));
     }
 
     @Test
     @Tag("stax")
     void hardenedStaxBlocks() {
-        AttackTestSupport.assertStaxBlocks(mediumXmlPayload());
+        AttackTestSupport.assertStaxBlocks(hardenedXmlPayload());
     }
 
     @Test
     @Tag("trax")
-    void hardenedTemplatesDoesNotLeak() {
-        
AttackTestSupport.assertTemplatesDoesNotLeak(AttackTestSupport.streamSource(xsltPayload()));
+    void hardenedTemplatesBlocks() {
+        
AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(hardenedXsltPayload()));
     }
 
     @Test
     @Tag("trax")
-    void hardenedTransformerDoesNotLeak() {
-        AttackTestSupport.assertTransformerDoesNotLeak(mediumXmlPayload());
+    void hardenedTransformerBlocks() {
+        AttackTestSupport.assertTransformerBlocks(hardenedXmlPayload());
     }
 
     @Test
     @Tag("schema")
     void hardenedValidatorBlocks() {
-        AttackTestSupport.assertValidatorBlocks(mediumXmlPayload());
+        AttackTestSupport.assertValidatorBlocks(hardenedXmlPayload());
     }
 
     @Test
@@ -179,45 +215,45 @@ void hardenedXmlReaderBlocks() {
 
     @Test
     @Tag("dom")
-    void unconfiguredDomResolves() {
+    void unconfiguredDomParses() {
         
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
                 "Skipped: platform DOM does not resolve user-defined 
entities");
-        AttackTestSupport.assertDomResolves(mediumXmlPayload());
+        AttackTestSupport.assertPermissiveDomParses(mediumXmlPayload());
     }
 
     @Test
     @Tag("sax")
-    void unconfiguredSaxResolves() {
-        AttackTestSupport.assertSaxResolves(mediumXmlPayload());
+    void unconfiguredSaxParses() {
+        AttackTestSupport.assertPermissiveSaxParses(mediumXmlPayload());
     }
 
     @Test
     @Tag("schema")
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(mediumXsdPayload()));
     }
 
     @Test
     @Tag("stax")
-    void unconfiguredStaxResolves() {
-        AttackTestSupport.assertStaxResolves(mediumXmlPayload());
+    void unconfiguredStaxParses() {
+        AttackTestSupport.assertPermissiveStaxParses(mediumXmlPayload());
     }
 
     @Test
     @Tag("trax")
     void unconfiguredTemplatesCompiles() {
-        AttackTestSupport.assertTemplatesCompiles(xsltPayload());
+        
AttackTestSupport.assertPermissiveTemplatesCompiles(mediumXsltPayload());
     }
 
     @Test
     @Tag("trax")
-    void unconfiguredTransformerSucceeds() {
-        AttackTestSupport.assertTransformerSucceeds(mediumXmlPayload());
+    void unconfiguredTransformerTransforms() {
+        
AttackTestSupport.assertPermissiveTransformerTransforms(mediumXmlPayload());
     }
 
     @Test
     @Tag("schema")
-    void unconfiguredValidatorAccepts() {
-        AttackTestSupport.assertValidatorAccepts(mediumXmlPayload());
+    void unconfiguredValidatorValidates() {
+        
AttackTestSupport.assertPermissiveValidatorValidates(mediumXmlPayload());
     }
 }
diff --git a/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java 
b/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java
index 451a2c8..6df4cf9 100644
--- a/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/DoctypeOnlyTest.java
@@ -16,6 +16,7 @@
  */
 package org.apache.commons.xml.factory;
 
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
@@ -45,6 +46,8 @@ private static String payload() {
     @Test
     @Tag("dom")
     void hardenedDomParses() {
+        
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
+                "Skipped: platform DOM does not resolve user-defined 
entities");
         AttackTestSupport.assertDomParses(payload());
     }
 
diff --git a/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java 
b/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java
index fd5e3cb..c164c20 100644
--- a/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/ExternalDtdTest.java
@@ -75,8 +75,8 @@ void hardenedSaxDoesNotLeak() {
 
     @Test
     @Tag("schema")
-    void hardenedSchemaBlocks() {
-        
AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload()));
+    void hardenedSchemaDoesNotLeak() {
+        
AttackTestSupport.assertSchemaDoesNotLeak(AttackTestSupport.streamSource(xsdPayload()));
     }
 
     @Test
@@ -87,8 +87,8 @@ void hardenedStaxBlocks() {
 
     @Test
     @Tag("trax")
-    void hardenedTemplatesBlocks() {
-        
AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload()));
+    void hardenedTemplatesDoesNotLeak() {
+        
AttackTestSupport.assertTemplatesDoesNotLeak(AttackTestSupport.streamSource(xsltPayload()));
     }
 
     @Test
@@ -99,8 +99,8 @@ void hardenedTransformerDoesNotLeak() {
 
     @Test
     @Tag("schema")
-    void hardenedValidatorBlocks() {
-        AttackTestSupport.assertValidatorBlocks(xmlPayload());
+    void hardenedValidatorDoesNotLeak() {
+        AttackTestSupport.assertValidatorDoesNotLeak(xmlPayload());
     }
 
     @Test
@@ -111,45 +111,45 @@ void hardenedXmlReaderDoesNotLeak() {
 
     @Test
     @Tag("dom")
-    void unconfiguredDomResolves() {
+    void unconfiguredDomParses() {
         
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
                 "Skipped: platform DOM does not resolve user-defined 
entities");
-        AttackTestSupport.assertDomResolves(xmlPayload());
+        AttackTestSupport.assertPermissiveDomParses(xmlPayload());
     }
 
     @Test
     @Tag("sax")
-    void unconfiguredSaxResolves() {
-        AttackTestSupport.assertSaxResolves(xmlPayload());
+    void unconfiguredSaxParses() {
+        AttackTestSupport.assertPermissiveSaxParses(xmlPayload());
     }
 
     @Test
     @Tag("schema")
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
     }
 
     @Test
     @Tag("stax")
-    void unconfiguredStaxResolves() {
-        AttackTestSupport.assertStaxResolves(xmlPayload());
+    void unconfiguredStaxParses() {
+        AttackTestSupport.assertPermissiveStaxParses(xmlPayload());
     }
 
     @Test
     @Tag("trax")
     void unconfiguredTemplatesCompiles() {
-        AttackTestSupport.assertTemplatesCompiles(xsltPayload());
+        AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload());
     }
 
     @Test
     @Tag("trax")
-    void unconfiguredTransformerSucceeds() {
-        AttackTestSupport.assertTransformerSucceeds(xmlPayload());
+    void unconfiguredTransformerTransforms() {
+        AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload());
     }
 
     @Test
     @Tag("schema")
-    void unconfiguredValidatorAccepts() {
-        AttackTestSupport.assertValidatorAccepts(xmlPayload());
+    void unconfiguredValidatorValidates() {
+        AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload());
     }
 }
diff --git 
a/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java 
b/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java
index 08c13f5..b1e7896 100644
--- 
a/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java
+++ 
b/src/test/java/org/apache/commons/xml/factory/ExternalGeneralEntityTest.java
@@ -113,45 +113,45 @@ void hardenedXmlReaderBlocks() {
 
     @Test
     @Tag("dom")
-    void unconfiguredDomResolves() {
+    void unconfiguredDomParses() {
         
Assumptions.assumeTrue(AttackTestSupport.DOM_RESOLVES_INTERNAL_ENTITIES,
                 "Skipped: platform DOM does not resolve user-defined 
entities");
-        AttackTestSupport.assertDomResolves(xmlPayload());
+        AttackTestSupport.assertPermissiveDomParses(xmlPayload());
     }
 
     @Test
     @Tag("sax")
-    void unconfiguredSaxResolves() {
-        AttackTestSupport.assertSaxResolves(xmlPayload());
+    void unconfiguredSaxParses() {
+        AttackTestSupport.assertPermissiveSaxParses(xmlPayload());
     }
 
     @Test
     @Tag("schema")
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
     }
 
     @Test
     @Tag("stax")
-    void unconfiguredStaxResolves() {
-        AttackTestSupport.assertStaxResolves(xmlPayload());
+    void unconfiguredStaxParses() {
+        AttackTestSupport.assertPermissiveStaxParses(xmlPayload());
     }
 
     @Test
     @Tag("trax")
     void unconfiguredTemplatesCompiles() {
-        AttackTestSupport.assertTemplatesCompiles(xsltPayload());
+        AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload());
     }
 
     @Test
     @Tag("trax")
-    void unconfiguredTransformerSucceeds() {
-        AttackTestSupport.assertTransformerSucceeds(xmlPayload());
+    void unconfiguredTransformerTransforms() {
+        AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload());
     }
 
     @Test
     @Tag("schema")
-    void unconfiguredValidatorAccepts() {
-        AttackTestSupport.assertValidatorAccepts(xmlPayload());
+    void unconfiguredValidatorValidates() {
+        AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload());
     }
 }
diff --git 
a/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java 
b/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java
index 353011f..3f7571b 100644
--- 
a/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java
+++ 
b/src/test/java/org/apache/commons/xml/factory/ExternalParameterEntityTest.java
@@ -157,6 +157,8 @@ void hardenedSaxBlocks() {
     @Test
     @Tag("schema")
     void hardenedSchemaBlocks() {
+        Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES,
+                "Skipped: platform SAX parser does not invoke the entity 
resolver for parameter entities");
         
AttackTestSupport.assertSchemaBlocks(AttackTestSupport.streamSource(xsdPayload()));
     }
 
@@ -169,18 +171,24 @@ void hardenedStaxBlocks() {
     @Test
     @Tag("trax")
     void hardenedTemplatesBlocks() {
+        Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES,
+                "Skipped: platform SAX parser does not invoke the entity 
resolver for parameter entities");
         
AttackTestSupport.assertTemplatesBlocks(AttackTestSupport.streamSource(xsltPayload()));
     }
 
     @Test
     @Tag("trax")
     void hardenedTransformerBlocks() {
+        Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES,
+                "Skipped: platform SAX parser does not invoke the entity 
resolver for parameter entities");
         AttackTestSupport.assertTransformerBlocks(xmlPayload());
     }
 
     @Test
     @Tag("schema")
     void hardenedValidatorBlocks() {
+        Assumptions.assumeTrue(SAX_RESOLVES_PARAMETER_ENTITIES,
+                "Skipped: platform SAX parser does not invoke the entity 
resolver for parameter entities");
         AttackTestSupport.assertValidatorBlocks(xmlPayload());
     }
 
@@ -194,45 +202,45 @@ void hardenedXmlReaderBlocks() {
 
     @Test
     @Tag("dom")
-    void unconfiguredDomResolves() {
+    void unconfiguredDomParses() {
         Assumptions.assumeTrue(DOM_ACCEPTS_PARAMETER_ENTITIES,
                 "Skipped: platform DOM does not accept parameter entities");
-        AttackTestSupport.assertDomResolves(xmlPayload());
+        AttackTestSupport.assertPermissiveDomParses(xmlPayload());
     }
 
     @Test
     @Tag("sax")
-    void unconfiguredSaxResolves() {
-        AttackTestSupport.assertSaxResolves(xmlPayload());
+    void unconfiguredSaxParses() {
+        AttackTestSupport.assertPermissiveSaxParses(xmlPayload());
     }
 
     @Test
     @Tag("schema")
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.streamSource(xsdPayload()));
     }
 
     @Test
     @Tag("stax")
-    void unconfiguredStaxResolves() {
-        AttackTestSupport.assertStaxResolves(xmlPayload());
+    void unconfiguredStaxParses() {
+        AttackTestSupport.assertPermissiveStaxParses(xmlPayload());
     }
 
     @Test
     @Tag("trax")
     void unconfiguredTemplatesCompiles() {
-        AttackTestSupport.assertTemplatesCompiles(xsltPayload());
+        AttackTestSupport.assertPermissiveTemplatesCompiles(xsltPayload());
     }
 
     @Test
     @Tag("trax")
-    void unconfiguredTransformerSucceeds() {
-        AttackTestSupport.assertTransformerSucceeds(xmlPayload());
+    void unconfiguredTransformerTransforms() {
+        AttackTestSupport.assertPermissiveTransformerTransforms(xmlPayload());
     }
 
     @Test
     @Tag("schema")
-    void unconfiguredValidatorAccepts() {
-        AttackTestSupport.assertValidatorAccepts(xmlPayload());
+    void unconfiguredValidatorValidates() {
+        AttackTestSupport.assertPermissiveValidatorValidates(xmlPayload());
     }
 }
diff --git a/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java 
b/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java
index aec609a..f9888db 100644
--- a/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/SchemaImportTest.java
@@ -35,6 +35,6 @@ void hardenedSchemaBlocks() {
 
     @Test
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
     }
 }
diff --git 
a/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java 
b/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java
index f02dbf1..229f988 100644
--- a/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/SchemaIncludeTest.java
@@ -35,6 +35,6 @@ void hardenedSchemaBlocks() {
 
     @Test
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
     }
 }
diff --git 
a/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java 
b/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java
index 734775a..5d10821 100644
--- a/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java
+++ b/src/test/java/org/apache/commons/xml/factory/SchemaRedefineTest.java
@@ -35,6 +35,6 @@ void hardenedSchemaBlocks() {
 
     @Test
     void unconfiguredSchemaCompiles() {
-        
AttackTestSupport.assertSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
+        
AttackTestSupport.assertPermissiveSchemaCompiles(AttackTestSupport.resourceSource(RESOURCE));
     }
 }


Reply via email to