garydgregory commented on code in PR #42:
URL: https://github.com/apache/commons-xml/pull/42#discussion_r3852807681


##########
src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java:
##########
@@ -304,6 +304,18 @@ private static LSInput lsInput(final String systemId) {
         }
     }
 
+    /** An {@link LSInput} naming the resource but carrying no content, the 
shape that would send the implementation into a default-resolution self-fetch. 
*/
+    private static LSInput identifierOnlyLsInput(final String systemId) {
+        try {
+            final DOMImplementationLS ls = (DOMImplementationLS) 
DOMImplementationRegistry.newInstance().getDOMImplementation("LS");
+            final LSInput input = ls.createLSInput();
+            input.setSystemId(systemId);
+            return input;
+        } catch (final Exception e) {
+            throw new IllegalStateException("Failed to build LSInput for " + 
systemId, e);

Review Comment:
   Do we want to wrap an existing `IllegalStateException`? If so, a comment to 
confirm this might avoid confusion.



##########
src/main/java/org/apache/commons/xml/FallbackIgnoreLSResourceResolver.java:
##########
@@ -60,10 +65,21 @@ LSResourceResolver getDelegate() {
         return delegate;
     }
 
+    /**
+     * Tells whether the input carries content, so the consumer never falls 
back to resolving its identifiers itself.
+     *
+     * @param input The input the caller's resolver returned.
+     * @return Whether a character stream, byte stream, or non-empty string 
data is present.
+     */
+    private static boolean hasContent(final LSInput input) {
+        // Empty string data counts as no content: the JDK's 
DOMEntityResolverWrapper discards it (see the unresolved branch below).
+        return input.getCharacterStream() != null || input.getByteStream() != 
null || input.getStringData() != null && !input.getStringData().isEmpty();
+    }
+
     @Override
     public LSInput resolveResource(final String type, final String 
namespaceURI, final String publicId, final String systemId, final String 
baseURI) {
         final LSInput resolved = delegate != null ? 
delegate.resolveResource(type, namespaceURI, publicId, systemId, baseURI) : 
null;
-        if (resolved != null) {
+        if (resolved != null && hasContent(resolved)) {

Review Comment:
   Why not test for null inside `hasContent()` since `null` has no content?



##########
src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java:
##########
@@ -325,6 +337,18 @@ void schemaDeniesUnlisted() {
         }, "Schema import", SAXException.class, SecurityException.class);
     }
 
+    @Test
+    @Tag("schema")
+    void schemaTreatsIdentifierOnlyOptInAsUnresolved() {
+        // Opting in requires supplying content: an identifier-only LSInput is 
treated as unresolved, so the import stays empty and the compile fails.
+        assertParseFails(() -> {
+            final SchemaFactory factory = 
XmlFactories.newSchemaFactory(XMLConstants.W3C_XML_SCHEMA_NS_URI);
+            factory.setResourceResolver((type, namespaceURI, publicId, 
systemId, baseURI) ->
+                    systemId != null && systemId.endsWith("included.xsd") ? 
identifierOnlyLsInput(ALLOWED_SCHEMA) : null);
+            
factory.newSchema(AttackTestSupport.resourceSource("with-import.xsd"));
+        }, "Schema import via identifier-only LSInput", SAXException.class, 
SecurityException.class);
+    }
+
     // ---- XSLT channel (URIResolver) 
----------------------------------------------------------------------------------------------------------------------

Review Comment:
   I usually remove AI generated noise like this // comment.
   



##########
src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java:
##########
@@ -356,6 +380,40 @@ void transformerDeniesUnlisted() {
         }
     }
 
+    @Test
+    @Tag("trax")
+    void transformerParsesOptedInImportHardened() {
+        // The opted-in module carries an external DTD reference; parsed on 
the floor the DTD is empty, so its entity cannot expand into the output.
+        final TransformerFactory factory = hardenedTransformerFactory();
+        factory.setURIResolver((href, base) ->
+                href != null && href.endsWith("included.xsl") ? 
AttackTestSupport.resourceSource("included-with-entity.xsl") : null);
+        try {
+            final StringWriter sink = new StringWriter();
+            
factory.newTemplates(AttackTestSupport.resourceSource("with-import.xsl")).newTransformer()
+                    .transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(sink));
+            
assertFalse(sink.toString().contains(AttackTestSupport.LEAKED_MARKER), 
"opted-in stylesheet import leaked its external entity");
+        } catch (final TransformerException blocked) {

Review Comment:
   This reads to me like the exception may or may not happen. 
   - If that's the case, then add a comment. 
   - If it's expected to always throw, then use an `assertThrows()` method.



##########
src/test/java/org/apache/commons/xml/EntityResolverFloorTest.java:
##########
@@ -356,6 +380,40 @@ void transformerDeniesUnlisted() {
         }
     }
 
+    @Test
+    @Tag("trax")
+    void transformerParsesOptedInImportHardened() {
+        // The opted-in module carries an external DTD reference; parsed on 
the floor the DTD is empty, so its entity cannot expand into the output.
+        final TransformerFactory factory = hardenedTransformerFactory();
+        factory.setURIResolver((href, base) ->
+                href != null && href.endsWith("included.xsl") ? 
AttackTestSupport.resourceSource("included-with-entity.xsl") : null);
+        try {
+            final StringWriter sink = new StringWriter();
+            
factory.newTemplates(AttackTestSupport.resourceSource("with-import.xsl")).newTransformer()
+                    .transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(sink));
+            
assertFalse(sink.toString().contains(AttackTestSupport.LEAKED_MARKER), 
"opted-in stylesheet import leaked its external entity");
+        } catch (final TransformerException blocked) {
+            // Acceptable: the hardened parse reports the entity as undeclared 
instead of expanding it.
+        }
+    }
+
+    @Test
+    @Tag("trax")
+    void transformerParsesOptedInDocumentHardened() {
+        // Same contract on the runtime document() channel, which reaches a 
different internal reader than the compile-time import.
+        final TransformerFactory factory = hardenedTransformerFactory();
+        factory.setURIResolver((href, base) ->
+                href != null && href.endsWith("referenced.xml") ? 
AttackTestSupport.resourceSource("referenced-with-entity.xml") : null);
+        try {
+            final StringWriter sink = new StringWriter();
+            
factory.newTemplates(AttackTestSupport.resourceSource("with-document.xsl")).newTransformer()
+                    .transform(AttackTestSupport.streamSource("<root/>"), new 
StreamResult(sink));
+            
assertFalse(sink.toString().contains(AttackTestSupport.LEAKED_MARKER), 
"opted-in document() resource leaked its external entity");
+        } catch (final TransformerException blocked) {
+            // Acceptable: the hardened parse reports the entity as undeclared 
instead of expanding it.

Review Comment:
   Same comment as above.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to