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

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


The following commit(s) were added to refs/heads/main by this push:
     new 81138e6  Keep a URIResolver a wrapped Transformer already carries (#96)
81138e6 is described below

commit 81138e66cda571f44d77d277e9ea0ffa53470476
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Mon Sep 14 01:09:20 2026 +0200

    Keep a URIResolver a wrapped Transformer already carries (#96)
    
    * Keep a URIResolver a wrapped Transformer already carries.
    
    The floor was installed with delegate.setURIResolver(floor) and seeded from
    the factory's resolver alone, so a resolver the delegate already carried was
    dropped unconsulted: document() then resolved to empty content instead of
    going through it. That contradicts the documented contract, where a caller's
    resolver is consulted before the floor and never replaced by it.
    
    The floor is now seeded with the resolver the delegate carries, falling back
    to the factory's where it carries none. A floor already in place came from
    this library, so it is skipped rather than nested, and no implementation
    seeds a fresh Transformer with a resolver that fetches: Xalan and Saxon
    leave it null, XSLTC copies only what its factory carried, and Saxon's own
    default resolution lives on the Configuration.
    
    The resolver threaded down from the factory is named factoryUriResolver
    throughout, since it is now the fallback rather than the only seed.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
    
    * Remove unused import for TemplatesHandler
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    
    * Pin what a reset restores.
    
    The floor is re-seeded on reset from the resolver the delegate carried, and
    the existing reset test only asked whether the floor still blocks, which a
    reset to the factory resolver would also satisfy. The new test opts a URI in
    through the carried resolver and expects it answered after the reset.
    
    Drops an unused import a test picked up along the way. Checkstyle does not
    see it: the pom leaves includeTestSourceDirectory commented out, so the
    rules run over main sources alone.
    
    Assisted-By: Claude Opus 5 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01B5Zpo8ypraR2meSvGudHQm
    
    ---------
    
    Co-authored-by: Copilot Autofix powered by AI 
<[email protected]>
    Co-authored-by: Gary Gregory <[email protected]>
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/xml/secure/SecureTemplates.java | 10 ++---
 .../commons/xml/secure/SecureTemplatesHandler.java | 10 ++---
 .../commons/xml/secure/SecureTransformer.java      | 25 ++++++-----
 .../xml/secure/SecureTransformerHandler.java       |  6 +--
 .../xml/secure/SecureTransformerHandlerTest.java   | 14 +++++++
 .../commons/xml/secure/SecureTransformerTest.java  | 45 ++++++++++++++++++++
 .../commons/xml/secure/TransformerHandlerTest.java | 49 ++++++++++++++++++++++
 .../apache/commons/xml/secure/XMLFilterTest.java   | 27 ++++++++++++
 9 files changed, 164 insertions(+), 23 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 837d9cd..f0b8ab4 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -37,6 +37,7 @@ The <action> type attribute can be add, update, fix, or 
remove.
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix the 
OpenRewrite migration recipe to add a dependency on 
org.apache.commons:commons-secure-xml:1.0.0.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Fix rejection behavior of foreign Templates in 
SAXTransformerFactory.newTransformerHandler.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Fix a NullPointerException when an XMLFilter with a self-driven parent 
reader is parsed with a null InputSource. #86</action>
+      <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory" issue="COMMONSXML-17">Keep a URIResolver already set on a Transformer 
when it is wrapped, instead of replacing it with the resolver floor.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Create the Transformer of an XMLFilter eagerly and reuse it for every 
parse.</action>
       <action type="fix" dev="pkarwasz" due-to="Piotr P. Karwasz, Gary 
Gregory">Report a stylesheet that produces no XMLFilter as a 
TransformerConfigurationException instead of returning null.</action>
       <!-- ADD -->
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureTemplates.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTemplates.java
index c276c0c..7025906 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTemplates.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTemplates.java
@@ -44,7 +44,7 @@ final class SecureTemplates implements Templates {
     /**
      * Compile-time URIResolver snapshot; the underlying implementation does 
not propagate the factory's resolver onto Transformers obtained from Templates.
      */
-    private final URIResolver uriResolver;
+    private final URIResolver factoryUriResolver;
 
     /**
      * Empty-{@link Source} supplier for the produced Transformer's floor; 
{@code null} means the default empty DOM.
@@ -61,14 +61,14 @@ final class SecureTemplates implements Templates {
      * Constructs a new instance.
      *
      * @param delegate         The delegate to wrap; must not be {@code null}.
-     * @param uriResolver      The compile-time URIResolver snapshot to 
restore onto Transformers produced from the compiled Templates; may be {@code 
null}.
+     * @param factoryUriResolver The factory's compile-time URIResolver 
snapshot to restore onto Transformers produced from the compiled Templates; may 
be {@code null}.
      * @param emptySource      The empty-{@link Source} supplier for the 
produced Transformers.
      * @param overrideDefaultParser whether the produced Transformers' source 
rewrites should use the pluggable parser lookup instead of the platform's 
built-in parser.
      * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
      */
-    SecureTemplates(final Templates delegate, final URIResolver uriResolver, 
final Supplier<Source> emptySource, final boolean overrideDefaultParser) {
+    SecureTemplates(final Templates delegate, final URIResolver 
factoryUriResolver, final Supplier<Source> emptySource, final boolean 
overrideDefaultParser) {
         this.delegate = Objects.requireNonNull(delegate, "delegate");
-        this.uriResolver = uriResolver;
+        this.factoryUriResolver = factoryUriResolver;
         this.emptySource = emptySource;
         this.overrideDefaultParser = overrideDefaultParser;
     }
@@ -92,6 +92,6 @@ public Transformer newTransformer() throws 
TransformerConfigurationException {
         final Transformer transformer = delegate.newTransformer();
         // Some implementations return null rather than throw, so preserve the 
delegate's behavior instead of enforcing the contract.
         // For example, https://issues.apache.org/jira/browse/XALANJ-2410
-        return transformer != null ? new SecureTransformer(transformer, 
uriResolver, emptySource, overrideDefaultParser) : null;
+        return transformer != null ? new SecureTransformer(transformer, 
factoryUriResolver, emptySource, overrideDefaultParser) : null;
     }
 }
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureTemplatesHandler.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTemplatesHandler.java
index 3b686d6..ec45cb8 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTemplatesHandler.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTemplatesHandler.java
@@ -44,7 +44,7 @@ final class SecureTemplatesHandler implements 
TemplatesHandler {
     /**
      * Compile-time URIResolver snapshot, restored onto Transformers produced 
from the compiled Templates.
      */
-    private final URIResolver uriResolver;
+    private final URIResolver factoryUriResolver;
 
     /**
      * Empty-{@link Source} supplier for the produced Templates' floor; {@code 
null} means the default empty DOM.
@@ -60,15 +60,15 @@ final class SecureTemplatesHandler implements 
TemplatesHandler {
      * Constructs a new instance.
      *
      * @param delegate The delegate to wrap; must not be {@code null}.
-     * @param uriResolver The compile-time URIResolver snapshot to restore 
onto Transformers produced from the compiled Templates; may be {@code null}.
+     * @param factoryUriResolver The factory's compile-time URIResolver 
snapshot to restore onto Transformers produced from the compiled Templates; may 
be {@code null}.
      * @param emptySource The empty-{@link Source} supplier for the produced 
Templates; may be {@code null} for the default empty DOM document.
      * @param overrideDefaultParser whether the produced Templates' source 
rewrites should use the pluggable parser lookup instead of the platform's 
built-in parser.
      * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
      */
-    SecureTemplatesHandler(final TemplatesHandler delegate, final URIResolver 
uriResolver, final Supplier<Source> emptySource,
+    SecureTemplatesHandler(final TemplatesHandler delegate, final URIResolver 
factoryUriResolver, final Supplier<Source> emptySource,
             final boolean overrideDefaultParser) {
         this.delegate = Objects.requireNonNull(delegate, "delegate");
-        this.uriResolver = uriResolver;
+        this.factoryUriResolver = factoryUriResolver;
         this.emptySource = emptySource;
         this.overrideDefaultParser = overrideDefaultParser;
     }
@@ -102,7 +102,7 @@ public String getSystemId() {
     public Templates getTemplates() {
         // Null before the stylesheet's endDocument (and on a failed compile 
in some implementations).
         final Templates templates = delegate.getTemplates();
-        return templates == null ? null : new SecureTemplates(templates, 
uriResolver, emptySource, overrideDefaultParser);
+        return templates == null ? null : new SecureTemplates(templates, 
factoryUriResolver, emptySource, overrideDefaultParser);
     }
 
     @Override
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
index d84127c..37a06ca 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformer.java
@@ -34,9 +34,9 @@
  * {@link SecureSAXParserFactory#secure(Source, boolean)} before delegating, 
and keeps an ignore-all {@link URIResolver} floor so runtime {@code document()} 
calls a
  * caller does not resolve return empty rather than being fetched.
  * <p>
- * The floor is installed on the delegate transformer at construction, seeded 
with the factory's compile-time resolver; {@link #setURIResolver(URIResolver)}
- * routes a caller's resolver through it rather than replacing it, so the 
block cannot be dropped. {@link #reset()} re-establishes the floor, seeded 
again with
- * the factory's compile-time resolver, matching the just-constructed state.
+ * The floor is installed on the delegate transformer at construction, seeded 
with the resolver the delegate already carried, or with the factory's
+ * compile-time resolver where it carried none; {@link 
#setURIResolver(URIResolver)} routes a caller's resolver through it rather than 
replacing it, so the
+ * block cannot be dropped. {@link #reset()} re-establishes the floor with 
that same seed, matching the just-constructed state.
  * </p>
  */
 final class SecureTransformer extends Transformer {
@@ -44,9 +44,10 @@ final class SecureTransformer extends Transformer {
     private final Transformer delegate;
 
     /**
-     * Compile-time URIResolver snapshot the floor is seeded with, both at 
construction and again on {@link #reset()}.
+     * URIResolver the floor is seeded with, both at construction and again on 
{@link #reset()}: the one the delegate carried, else the factory's compile-time
+     * snapshot.
      */
-    private final URIResolver uriResolver;
+    private final URIResolver initialUriResolver;
 
     private final FallbackIgnoreURIResolver floor;
 
@@ -60,16 +61,20 @@ final class SecureTransformer extends Transformer {
      * Constructs a new instance.
      *
      * @param delegate         The delegate to wrap; must not be {@code null}.
-     * @param uriResolver      The compile-time URIResolver snapshot to seed 
the floor with; may be {@code null}.
+     * @param factoryUriResolver The factory's compile-time URIResolver 
snapshot, used where the delegate carries none of its own; may be {@code null}.
      * @param emptySource      The empty-{@link Source} supplier for the 
produced Transformers; {@code null} for the default empty DOM document.
      * @param overrideDefaultParser whether the source rewrites should use the 
pluggable parser lookup instead of the platform's built-in parser.
      * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
      */
-    SecureTransformer(final Transformer delegate, final URIResolver 
uriResolver, final Supplier<Source> emptySource, final boolean 
overrideDefaultParser) {
+    SecureTransformer(final Transformer delegate, final URIResolver 
factoryUriResolver, final Supplier<Source> emptySource,
+            final boolean overrideDefaultParser) {
         this.delegate = Objects.requireNonNull(delegate, "delegate");
-        this.uriResolver = uriResolver;
         this.overrideDefaultParser = overrideDefaultParser;
-        this.floor = new FallbackIgnoreURIResolver(uriResolver, emptySource, 
() -> overrideDefaultParser);
+        // A caller may have configured the delegate before it reached us; 
chain the floor onto that resolver rather than dropping it. A floor already 
there
+        // came from this library, so it is the factory's resolver that seeds 
the new one.
+        final URIResolver carried = delegate.getURIResolver();
+        this.initialUriResolver = carried == null || carried instanceof 
FallbackIgnoreURIResolver ? factoryUriResolver : carried;
+        this.floor = new FallbackIgnoreURIResolver(initialUriResolver, 
emptySource, () -> overrideDefaultParser);
         delegate.setURIResolver(floor);
     }
 
@@ -106,7 +111,7 @@ public URIResolver getURIResolver() {
     @Override
     public void reset() {
         delegate.reset();
-        floor.setDelegate(uriResolver);
+        floor.setDelegate(initialUriResolver);
         delegate.setURIResolver(floor);
     }
 
diff --git 
a/src/main/java/org/apache/commons/xml/secure/SecureTransformerHandler.java 
b/src/main/java/org/apache/commons/xml/secure/SecureTransformerHandler.java
index f90cb53..7daaefc 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureTransformerHandler.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureTransformerHandler.java
@@ -52,15 +52,15 @@ final class SecureTransformerHandler implements 
TransformerHandler {
      * Constructs a new instance.
      *
      * @param delegate The delegate to wrap; must not be {@code null}.
-     * @param uriResolver The compile-time URIResolver snapshot to restore 
onto the live transformer; may be {@code null}.
+     * @param factoryUriResolver The factory's compile-time URIResolver 
snapshot to restore onto the live transformer; may be {@code null}.
      * @param emptySource The empty-{@link Source} supplier for the produced 
Transformer's floor; {@code null} means the default empty DOM.
      * @param overrideDefaultParser whether the live transformer's source 
rewrites should use the pluggable parser lookup instead of the platform's 
built-in parser.
      * @throws NullPointerException Thrown if {@code delegate} is {@code null}.
      */
-    SecureTransformerHandler(final TransformerHandler delegate, final 
URIResolver uriResolver, final Supplier<Source> emptySource,
+    SecureTransformerHandler(final TransformerHandler delegate, final 
URIResolver factoryUriResolver, final Supplier<Source> emptySource,
             final boolean overrideDefaultParser) {
         this.delegate = Objects.requireNonNull(delegate, "delegate");
-        this.transformer = new SecureTransformer(delegate.getTransformer(), 
uriResolver, emptySource, overrideDefaultParser);
+        this.transformer = new SecureTransformer(delegate.getTransformer(), 
factoryUriResolver, emptySource, overrideDefaultParser);
     }
 
     @Override
diff --git 
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerHandlerTest.java 
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerHandlerTest.java
index d65b86d..f0300d8 100644
--- 
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerHandlerTest.java
+++ 
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerHandlerTest.java
@@ -19,11 +19,14 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
 
 import java.io.StringWriter;
 
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
 import javax.xml.transform.sax.SAXTransformerFactory;
+import javax.xml.transform.sax.TransformerHandler;
 import javax.xml.transform.stream.StreamResult;
 
 import org.junit.jupiter.api.Tag;
@@ -34,6 +37,17 @@
 @Tag("trax")
 class SecureTransformerHandlerTest {
 
+    @Test
+    void adoptsAResolverTheHandlersTransformerCarries() throws Exception {
+        final SAXTransformerFactory factory = (SAXTransformerFactory) 
TransformerFactory.newInstance();
+        final TransformerHandler delegate = factory.newTransformerHandler();
+        // An implementation may seed the handler's transformer from the 
Templates it was built with; that resolver must survive the wrapping.
+        final URIResolver carried = (href, base) -> null;
+        delegate.getTransformer().setURIResolver(carried);
+        final SecureTransformerHandler handler = new 
SecureTransformerHandler(delegate, null, null, false);
+        assertSame(carried, handler.getTransformer().getURIResolver(), "the 
resolver the handler's transformer carried must survive the wrapping");
+    }
+
     @Test
     void forwardsEveryTransformerHandlerMethod() throws Exception {
         final SAXTransformerFactory factory = (SAXTransformerFactory) 
TransformerFactory.newInstance();
diff --git 
a/src/test/java/org/apache/commons/xml/secure/SecureTransformerTest.java 
b/src/test/java/org/apache/commons/xml/secure/SecureTransformerTest.java
index dbf6009..e50996b 100644
--- a/src/test/java/org/apache/commons/xml/secure/SecureTransformerTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/SecureTransformerTest.java
@@ -17,7 +17,10 @@
 
 package org.apache.commons.xml.secure;
 
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.StringReader;
 import java.io.StringWriter;
@@ -26,8 +29,10 @@
 import javax.xml.parsers.DocumentBuilderFactory;
 import javax.xml.transform.ErrorListener;
 import javax.xml.transform.OutputKeys;
+import javax.xml.transform.Transformer;
 import javax.xml.transform.TransformerException;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
 import javax.xml.transform.dom.DOMSource;
 import javax.xml.transform.stream.StreamResult;
 import javax.xml.transform.stream.StreamSource;
@@ -38,6 +43,46 @@
 @Tag("trax")
 class SecureTransformerTest {
 
+    /** A transformer a caller configured before this library saw it, the 
shape a caller's own Templates hands out. */
+    private static SecureTransformer wrap(final URIResolver carried) throws 
Exception {
+        final Transformer delegate = 
TransformerFactory.newInstance().newTransformer(AttackTestSupport.resourceSource("with-document.xsl"));
+        delegate.setURIResolver(carried);
+        return new SecureTransformer(delegate, null, null, false);
+    }
+
+    @Test
+    void adoptsAResolverTheDelegateAlreadyCarries() throws Exception {
+        final URIResolver carried = (href, base) -> new StreamSource(new 
StringReader("<opted-in/>"));
+        final SecureTransformer transformer = wrap(carried);
+        assertSame(carried, transformer.getURIResolver(), "the resolver the 
delegate carried must survive the wrapping");
+        final StringWriter output = new StringWriter();
+        transformer.transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(output));
+        assertTrue(output.toString().contains("opted-in"), "the carried 
resolver must answer document()");
+        
assertFalse(output.toString().contains(AttackTestSupport.LEAKED_MARKER), "the 
real resource must not be fetched");
+    }
+
+    @Test
+    void keepsTheFloorUnderACarriedResolverThatDeclines() throws Exception {
+        // Adopting the caller's resolver must not make the floor reachable 
around: what the resolver declines stays unfetched.
+        final SecureTransformer transformer = wrap((href, base) -> null);
+        final StringWriter output = new StringWriter();
+        transformer.transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(output));
+        
assertFalse(output.toString().contains(AttackTestSupport.LEAKED_MARKER), 
"document() the resolver declined must not be fetched");
+    }
+
+    @Test
+    void carriesTheAdoptedResolverThroughReset() throws Exception {
+        final URIResolver carried = (href, base) -> new StreamSource(new 
StringReader("<opted-in/>"));
+        final SecureTransformer transformer = wrap(carried);
+        // reset() re-seeds the floor, and the seed is the resolver the 
delegate carried, not the factory's.
+        transformer.reset();
+        assertSame(carried, transformer.getURIResolver(), "reset must restore 
the resolver the delegate carried");
+        final StringWriter output = new StringWriter();
+        transformer.transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(output));
+        assertTrue(output.toString().contains("opted-in"), "the carried 
resolver must still answer document() after a reset");
+        
assertFalse(output.toString().contains(AttackTestSupport.LEAKED_MARKER), "the 
real resource must not be fetched");
+    }
+
     @Test
     void forwardsEveryTransformerMethod() throws Exception {
         final TransformerFactory factory = TransformerFactory.newInstance();
diff --git 
a/src/test/java/org/apache/commons/xml/secure/TransformerHandlerTest.java 
b/src/test/java/org/apache/commons/xml/secure/TransformerHandlerTest.java
index 5ac1a9a..06d35be 100644
--- a/src/test/java/org/apache/commons/xml/secure/TransformerHandlerTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/TransformerHandlerTest.java
@@ -19,16 +19,22 @@
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import java.io.StringWriter;
+import java.util.Properties;
 
 import javax.xml.transform.Templates;
+import javax.xml.transform.Transformer;
+import javax.xml.transform.TransformerConfigurationException;
 import javax.xml.transform.TransformerFactory;
+import javax.xml.transform.URIResolver;
 import javax.xml.transform.sax.SAXTransformerFactory;
 import javax.xml.transform.sax.TransformerHandler;
 import javax.xml.transform.stream.StreamResult;
 
+import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
@@ -65,6 +71,49 @@ void secureTransformerHandlerDoesNotLeakDocument() throws 
Exception {
         
assertFalse(transformViaHandler(handler).contains(AttackTestSupport.LEAKED_MARKER),
 "document() through TransformerHandler leaked");
     }
 
+    /** A caller's own Templates that only configures the Transformer it hands 
out, the shape Apache CXF's XSLTJaxbProvider builds. */
+    private static Templates callersTemplates(final Templates compiled, final 
URIResolver carried) {
+        return new Templates() {
+
+            @Override
+            public Properties getOutputProperties() {
+                return compiled.getOutputProperties();
+            }
+
+            @Override
+            public Transformer newTransformer() throws 
TransformerConfigurationException {
+                final Transformer transformer = compiled.newTransformer();
+                transformer.setURIResolver(carried);
+                return transformer;
+            }
+        };
+    }
+
+    /** Skips the test where the implementation refuses a Templates it did not 
compile itself, as Saxon does. */
+    private static TransformerHandler assumeAcceptsForeignImplementation(final 
Templates callers) {
+        try {
+            return ((SAXTransformerFactory) 
TransformerFactory.newInstance()).newTransformerHandler(callers);
+        } catch (final TransformerConfigurationException e) {
+            Assumptions.abort("the implementation does not accept a foreign 
Templates: " + e.getMessage());
+            return null;
+        }
+    }
+
+    @Test
+    void secureHandlerKeepsAResolverTheCallersTemplatesSet() throws Exception {
+        final Templates compiled = 
TransformerFactory.newInstance().newTemplates(AttackTestSupport.resourceSource("with-document.xsl"));
+        final URIResolver carried = (href, base) -> null;
+        final Templates callers = callersTemplates(compiled, carried);
+        // What the implementation itself ends up with: XSLTC leaves the 
caller's resolver in place, Apache Xalan overwrites it with its factory's own.
+        final TransformerHandler nativeTransformerHandler = 
assumeAcceptsForeignImplementation(callers);
+        
Assumptions.assumeTrue(nativeTransformerHandler.getTransformer().getURIResolver()
 == carried,
+                "the implementation does not keep a resolver set by the 
caller's Templates");
+
+        final TransformerHandler handler = 
SaxSurfaceTestSupport.secureFactory().newTransformerHandler(callers);
+        assertSame(carried, handler.getTransformer().getURIResolver(), "the 
securing must not lose a resolver the implementation kept");
+        
assertFalse(transformViaHandler(handler).contains(AttackTestSupport.LEAKED_MARKER),
 "what that resolver declined must not be fetched");
+    }
+
     @Test
     void secureTransformerHandlerFromTemplatesDoesNotLeakDocument() throws 
Exception {
         final SAXTransformerFactory factory = 
SaxSurfaceTestSupport.secureFactory();
diff --git a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java 
b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
index e1cedd3..9af57dd 100644
--- a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
@@ -26,12 +26,16 @@
 import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Properties;
 
 import javax.xml.XMLConstants;
 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.SAXTransformerFactory;
+import javax.xml.transform.stream.StreamSource;
 
 import org.junit.jupiter.api.Assumptions;
 import org.junit.jupiter.api.Tag;
@@ -203,6 +207,29 @@ void secureFilterFromTemplatesDoesNotLeakDocument() throws 
Exception {
         assertFalse(filterAndCapture(filter, 
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() through 
XMLFilter(Templates) leaked");
     }
 
+    @Test
+    void secureFilterKeepsAResolverTheCallersTemplatesSet() throws Exception {
+        final Templates compiled = 
TransformerFactory.newInstance().newTemplates(AttackTestSupport.resourceSource("with-document.xsl"));
+        // A caller's own Templates that configures the Transformer it hands 
out, the shape Apache CXF's XSLTJaxbProvider builds.
+        final Templates callers = new Templates() {
+
+            @Override
+            public Properties getOutputProperties() {
+                return compiled.getOutputProperties();
+            }
+
+            @Override
+            public Transformer newTransformer() throws 
TransformerConfigurationException {
+                final Transformer transformer = compiled.newTransformer();
+                transformer.setURIResolver((href, base) -> new 
StreamSource(new StringReader("<opted-in>resolver-applied</opted-in>")));
+                return transformer;
+            }
+        };
+        final String output = 
filterAndCapture(SaxSurfaceTestSupport.secureFactory().newXMLFilter(callers), 
"<root/>");
+        assertTrue(output.contains("resolver-applied"), "the resolver the 
caller's Templates set must answer document()");
+        assertFalse(output.contains(AttackTestSupport.LEAKED_MARKER), "the 
real resource must not be fetched");
+    }
+
     @Test
     void secureFilterParsesWithoutAnInputSource() throws Exception {
         // A SAXSource carrying only the filter unmarshals as 
parse((InputSource) null): the input comes from the parent, and implementations 
that dereference

Reply via email to