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 fb87d7e  Substitute an InputSource when an XMLFilter is parsed with 
`null` (#86)
fb87d7e is described below

commit fb87d7e8471e850600f5e043c789e8c226856a27
Author: Piotr P. Karwasz <[email protected]>
AuthorDate: Sun Sep 13 19:02:51 2026 +0200

    Substitute an InputSource when an XMLFilter is parsed with `null` (#86)
    
    * Substitute an InputSource when an XMLFilter is parsed with none.
    
    A caller that drives an XMLFilter from a self-driven parent reader has no
    InputSource to hand it: unmarshalling a SAXSource that carries only a
    reader enters parse with a null one. Most TrAX implementations dereference
    the InputSource of a SAXSource without a check, so pass on an empty one
    under a placeholder URN rather than the null.
    
    Assisted-By: Claude Opus 5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01AaDyR9HjZLWkFn42x7kje3
    
    * fix: add reference to Xalan bug
    
    * fix: add reference to PR in changelog
---
 src/changes/changes.xml                            |   1 +
 .../apache/commons/xml/secure/SecureXMLFilter.java |   7 +-
 .../apache/commons/xml/secure/XMLFilterTest.java   | 129 ++++++++++++---------
 3 files changed, 84 insertions(+), 53 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 007e79e..708c972 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -35,6 +35,7 @@ The <action> type attribute can be add, update, fix, or 
remove.
       <!-- FIX -->
       <action type="fix" dev="ggregory" due-to="Gary Gregory">Fix the 
OpenRewrite migration recipe to target static method calls instead of class 
references.</action>
       <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 a NullPointerException when an XMLFilter with a self-driven parent 
reader is parsed with a null InputSource. #86</action>
       <!-- ADD -->
       <!-- UPDATE -->
       <action dev="ggregory" type="update" due-to="Gary Gregory">Bump 
org.apache.commons:commons-parent from 104 to 105.</action>
diff --git a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java 
b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
index 3f8f89c..1287c65 100644
--- a/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
+++ b/src/main/java/org/apache/commons/xml/secure/SecureXMLFilter.java
@@ -49,6 +49,9 @@
  */
 final class SecureXMLFilter extends XMLFilterImpl implements ErrorListener {
 
+    /** System id of the {@link InputSource} substituted for a caller's {@code 
null} one in {@link #parse(InputSource)}; a URN, so nothing can fetch it. */
+    static final String NO_INPUT_SYSTEM_ID = 
"urn:uuid:a47f732e-9111-42db-b648-5e24b7d663f3";
+
     /**
      * Bridges a TrAX report to the SAX callback shape.
      *
@@ -137,7 +140,9 @@ public void parse(final InputSource input) throws 
SAXException, IOException {
             final Transformer transformer = templates.newTransformer();
             // The filter is the listener, so TrAX error reports reach the 
caller-set ErrorHandler like the parent reader's SAX reports do.
             transformer.setErrorListener(this);
-            transformer.transform(new SAXSource(parent, input), result);
+            // A self-driven parent needs no InputSource, so a caller may pass 
null here; most TrAX implementations dereference the one they get unchecked.
+            // See: https://issues.apache.org/jira/browse/XALANJ-2851
+            transformer.transform(new SAXSource(parent, input != null ? input 
: new InputSource(NO_INPUT_SYSTEM_ID)), result);
         } catch (final TransformerException e) {
             // The parent reader's parse errors and the handler's own 
exceptions arrive wrapped; rethrow the original rather than nesting the 
hierarchies.
             final Throwable cause = e.getCause();
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 9cc55c8..d57e355 100644
--- a/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
+++ b/src/test/java/org/apache/commons/xml/secure/XMLFilterTest.java
@@ -56,6 +56,66 @@
 @Tag("trax")
 class XMLFilterTest {
 
+    /**
+     * Parent reader that drives itself, the shape a caller hands the filter 
when the source document does not come from an {@code InputSource}.
+     *
+     * <p>Records the callbacks the filter wires onto it, and the {@code 
InputSource} it is driven with.</p>
+     */
+    private static final class SelfDrivenParent extends XMLFilterImpl {
+
+        final List<InputSource> parsed = new ArrayList<>();
+
+        final List<Object> wired = new ArrayList<>();
+
+        @Override
+        public boolean getFeature(final String name) {
+            // Accept the namespace probes implementations make on a SAXSource 
reader; there is no parent to delegate to.
+            return "http://xml.org/sax/features/namespaces".equals(name);
+        }
+
+        @Override
+        public Object getProperty(final String name) {
+            return null;
+        }
+
+        @Override
+        public void parse(final InputSource input) throws SAXException {
+            parsed.add(input);
+            // Minimal well-formed document for the transformation to consume; 
no real parser behind this parent.
+            final ContentHandler handler = getContentHandler();
+            handler.startDocument();
+            handler.startElement("", "root", "root", new AttributesImpl());
+            handler.endElement("", "root", "root");
+            handler.endDocument();
+        }
+
+        @Override
+        public void setDTDHandler(final DTDHandler handler) {
+            wired.add(handler);
+            super.setDTDHandler(handler);
+        }
+
+        @Override
+        public void setEntityResolver(final EntityResolver resolver) {
+            wired.add(resolver);
+            super.setEntityResolver(resolver);
+        }
+
+        @Override
+        public void setErrorHandler(final ErrorHandler handler) {
+            wired.add(handler);
+            super.setErrorHandler(handler);
+        }
+
+        @Override
+        public void setFeature(final String name, final boolean value) {
+        }
+
+        @Override
+        public void setProperty(final String name, final Object value) {
+        }
+    }
+
     /** Copies the input through unchanged, so external-entity content in the 
input would surface in the filter's output. */
     private static final String IDENTITY_XSLT = "<xsl:stylesheet 
version=\"1.0\" xmlns:xsl=\"http://www.w3.org/1999/XSL/Transform\";>\n"
             + "  <xsl:template match=\"/\"><xsl:copy-of 
select=\".\"/></xsl:template>\n"
@@ -133,6 +193,20 @@ void secureFilterFromTemplatesDoesNotLeakDocument() throws 
Exception {
         assertFalse(filterAndCapture(filter, 
"<root/>").contains(AttackTestSupport.LEAKED_MARKER), "document() through 
XMLFilter(Templates) leaked");
     }
 
+    @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
+        // the InputSource the filter passes on must still get one.
+        final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
+        final SelfDrivenParent parent = new SelfDrivenParent();
+        filter.setParent(parent);
+        filter.setContentHandler(new DefaultHandler());
+        filter.parse((InputSource) null);
+        final InputSource substituted = parent.parsed.get(0);
+        assertNotNull(substituted, "the parent must be handed an InputSource, 
not the caller's null");
+        assertEquals(SecureXMLFilter.NO_INPUT_SYSTEM_ID, 
substituted.getSystemId(), "the substituted InputSource must carry the 
placeholder system id");
+    }
+
     @Test
     void secureFilterRethrowsHandlerSAXException() throws Exception {
         final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
@@ -170,60 +244,11 @@ void secureFilterWiresCallbacksToParent() throws 
Exception {
         // parse must perform the XMLFilterImpl.setupParse wiring on the 
parent for the resolver, DTD and error callbacks (the ContentHandler is owned 
by the
         // transformer). The wiring calls are asserted directly on a recording 
parent: which of them the implementation later consults or overwrites varies.
         final XMLFilter filter = 
SaxSurfaceTestSupport.secureFactory().newXMLFilter(AttackTestSupport.streamSource(IDENTITY_XSLT));
-        final List<Object> wired = new ArrayList<>();
-        final XMLFilterImpl parent = new XMLFilterImpl() {
-
-            @Override
-            public boolean getFeature(final String name) {
-                // Accept the namespace probes implementations make on a 
SAXSource reader; there is no parent to delegate to.
-                return "http://xml.org/sax/features/namespaces".equals(name);
-            }
-
-            @Override
-            public Object getProperty(final String name) {
-                return null;
-            }
-
-            @Override
-            public void parse(final InputSource input) throws SAXException {
-                // Minimal well-formed document for the transformation to 
consume; no real parser behind this parent.
-                final ContentHandler handler = getContentHandler();
-                handler.startDocument();
-                handler.startElement("", "root", "root", new AttributesImpl());
-                handler.endElement("", "root", "root");
-                handler.endDocument();
-            }
-
-            @Override
-            public void setDTDHandler(final DTDHandler handler) {
-                wired.add(handler);
-                super.setDTDHandler(handler);
-            }
-
-            @Override
-            public void setEntityResolver(final EntityResolver resolver) {
-                wired.add(resolver);
-                super.setEntityResolver(resolver);
-            }
-
-            @Override
-            public void setErrorHandler(final ErrorHandler handler) {
-                wired.add(handler);
-                super.setErrorHandler(handler);
-            }
-
-            @Override
-            public void setFeature(final String name, final boolean value) {
-            }
-
-            @Override
-            public void setProperty(final String name, final Object value) {
-            }
-        };
+        final SelfDrivenParent parent = new SelfDrivenParent();
         filter.setParent(parent);
         assertEquals("", filterAndCapture(filter, "<ignored/>"));
-        assertEquals(3, wired.stream().filter(callback -> callback == 
filter).count(),
-                "parse should wire the filter as the parent's EntityResolver, 
DTDHandler and ErrorHandler: " + wired);
+        assertEquals(3, parent.wired.stream().filter(callback -> callback == 
filter).count(),
+                "parse should wire the filter as the parent's EntityResolver, 
DTDHandler and ErrorHandler: " + parent.wired);
     }
 
     @Test

Reply via email to